--- /dev/null
+---
+Language: Cpp
+# BasedOnStyle: LLVM
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: false
+AlignConsecutiveAssignments: false
+AlignConsecutiveBitFields: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Right
+AlignOperands: Align
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortEnumsOnASingleLine: false
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Empty
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ AfterExternBlock: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+ColumnLimit: 100
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '^"(qcoro)/'
+ Priority: 2
+ SortPriority: 0
+ - Regex: '^(<|"(gtest|gmock|isl|json)/)'
+ Priority: 3
+ SortPriority: 0
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+ReflowComments: false
+SortIncludes: true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+Standard: Latest
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+ - addTest
+TabWidth: 8
+UseCRLF: false
+UseTab: Never
+WhitespaceSensitiveMacros:
+ - STRINGIZE
+ - PP_STRINGIZE
+ - BOOST_PP_STRINGIZE
+...
+
--- /dev/null
+# Initial run of clang-format
+0c01ca40fd8bd337ecae3ea21be1e5a97ba40e4b
--- /dev/null
+# Thanks for any donations! :)
+
+github: [danvratil]
+liberapay: dvratil
+ko_fi: dvratil
+custom: ['https://paypal.me/dvratil']
--- /dev/null
+name: Install Compiler
+description: Installs compiler
+inputs:
+ compiler:
+ description: Compiler to install (gcc|clang)
+ required: true
+ version:
+ description: Version of compiler to install
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install GCC
+ if: ${{ inputs.compiler == 'gcc' }}
+ shell: bash
+ run: |
+ GCC_VERSION_MAJOR=$(echo ${{ inputs.version }} | cut -d '.' -f1)
+ sudo add-apt-repository "deb https://ppa.launchpadcontent.net/ubuntu-toolchain-r/ppa/ubuntu focal main"
+ sudo apt-get update
+ sudo apt-get install -y \
+ gcc-${GCC_VERSION_MAJOR} g++-${GCC_VERSION_MAJOR} libstdc++-${GCC_VERSION_MAJOR}-dev
+ echo "CXX=/usr/bin/g++-${GCC_VERSION_MAJOR}" >> $GITHUB_ENV
+ - name: Install Clang
+ if : ${{ inputs.compiler == 'clang' }}
+ shell: bash
+ run: |
+ wget https://apt.llvm.org/llvm.sh -O llvm.sh
+ chmod a+x llvm.sh
+ sed -i "s/libunwind-\$LLVM_VERSION-dev//" llvm.sh
+ sudo ./llvm.sh ${{ inputs.version }} all
+ echo "CXX=/usr/bin/clang++-${{ inputs.version }}" >> $GITHUB_ENV
\ No newline at end of file
--- /dev/null
+name: 'Install Qt'
+description: 'Install Qt'
+inputs:
+ qt_version:
+ description: 'Version of Qt to install'
+ required: true
+ qt_archives:
+ description: 'List of Qt archives to intall'
+ qt_modules:
+ description: 'List of Qt modules to install'
+ platform:
+ description: 'Operating system (linux|windows|macos)'
+ required: true
+runs:
+ using: composite
+ steps:
+ - name: Install dependencies
+ if: ${{ inputs.platform == 'linux' }}
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends \
+ python3 python3-pip \
+ build-essential \
+ dbus dbus-x11 \
+ libgl-dev libegl-dev
+ - name: Install Qt
+ shell: bash
+ run: |
+ pip3 install aqtinstall~=2.1
+ case "${{ inputs.platform }}" in
+ "windows")
+ QT_ARCH="win64_msvc2019_64"
+ QT_INSTALL_PATH="C:\\Qt"
+ QT_BASE_DIR="${QT_INSTALL_PATH}\\${{ inputs.qt_version }}\\msvc2019_64"
+ QT_OS="windows"
+ ;;
+ "linux")
+ QT_ARCH="gcc_64"
+ QT_INSTALL_PATH="/opt/qt"
+ QT_BASE_DIR="${QT_INSTALL_PATH}/${{ inputs.qt_version }}/${QT_ARCH}"
+ QT_OS="linux"
+ ;;
+ "macos")
+ QT_ARCH=""
+ QT_INSTALL_PATH="/Users/runner/qt"
+ if [[ "$(echo ${{ inputs.qt_version }} | cut -d'.' -f1)" == "6" ]]; then
+ QT_BASE_DIR="${QT_INSTALL_PATH}/${{ inputs.qt_version }}/macos"
+ else
+ QT_BASE_DIR="${QT_INSTALL_PATH}/${{ inputs.qt_version }}/clang_64"
+ fi
+ QT_OS="mac"
+ ;;
+ esac
+
+ if [[ -n "${{ inputs.qt_archives }}" ]]; then
+ qt_archives="--archives ${{ inputs.qt_archives }}"
+ fi
+ if [[ -n "${{ inputs.qt_modules }}" ]]; then
+ qt_modules="--modules ${{ inputs.qt_modules }}"
+ fi
+ python3 -m aqt install-qt -O ${QT_INSTALL_PATH} ${QT_OS} desktop ${{ inputs.qt_version }} ${QT_ARCH} ${qt_archives} ${qt_modules}
+
+ echo "${QT_BASE_DIR}/bin" >> $GITHUB_PATH
+ echo "CMAKE_PREFIX_PATH=${QT_BASE_DIR}/lib/cmake" >> $GITHUB_ENV
+ echo "LD_LIBRARY_PATH=${QT_BASE_DIR}/lib:${LD_LIBRARY_PATH}" >> $GITHUB_ENV
+ echo "XDG_DATA_DIRS=${QT_BASE_DIR}/share:${XDG_DATA_DIRS}" >> $GITHUB_ENV
+
+ if [[ "${{ inputs.platform }}" == "windows" ]]; then
+ powershell "./.github/actions/install-qt/install-dbus.ps1" "$QT_BASE_DIR"
+ fi
--- /dev/null
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
+Set-PSRepository -Name 'PSGallery' -SourceLocation "https://www.powershellgallery.com/api/v2" -InstallationPolicy Trusted
+Install-Module -Name 7Zip4PowerShell -Force
+
+Invoke-WebRequest -Uri https://files.kde.org/craft/master/23.09/windows/cl/msvc2019/x86_64/RelWithDebInfo/libs/dbus/dbus-1.14.8-20230912T142911-windows-cl-msvc2019-x86_64.7z -OutFile C:\Qt\dbus.7z
+Expand-7Zip -ArchiveFileName C:\Qt\dbus.7z -TargetPath $args[0]
+
--- /dev/null
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
--- /dev/null
+name: Build Docker images
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'docker/**'
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+ paths:
+ - 'docker/**'
+ workflow_dispatch:
+
+env:
+ BUILD_TYPE: RelWithDebInfo
+ QTEST_FUNCTION_TIMEOUT: 60000
+
+jobs:
+ generate-matrix:
+ name: Generate build matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: set-matrix
+ name: Generate matrix
+ run: |
+ matrix_json=$(python3 ./.github/workflows/generate-matrix.py --platform linux)
+ echo "::set-output name=matrix::${matrix_json}"
+
+ build:
+ needs: generate-matrix
+ strategy:
+ matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
+ fail-fast: false
+
+ runs-on: ${{ matrix.runs_on }}
+ name: Build Docker image for ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+
+ - name: Docker Buildx setup
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to GCR
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository }}/build-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+
+ - name: Build and push image
+ uses: docker/build-push-action@v6
+ with:
+ build-args: |
+ compiler_image=${{ matrix.compiler_base_image }}
+ compiler_version=${{ matrix.compiler_version }}
+ qt_version=${{ matrix.qt_version }}
+ qt_modules=${{ matrix.qt_modules }}
+ qt_archives=${{ matrix.qt_archives }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ file: docker/Dockerfile.${{ matrix.compiler }}
+ context: docker
--- /dev/null
+name: Linux CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+env:
+ BUILD_TYPE: Debug
+ QTEST_FUNCTION_TIMEOUT: 60000
+
+
+jobs:
+ detect_run:
+ name: Check changes
+ outputs: { "source_code_changed": "${{ steps.source_code_changed.outputs.source_code_changed}}" }
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: changed_files
+ uses: tj-actions/changed-files@v46
+ name: Get changed files
+ with:
+ files_ignore: |
+ docs/**
+ docker/**
+ requirements.txt
+ mkdocs.yml
+ README.md
+ .github/**
+ - id: source_code_changed
+ name: Check for source code changes
+ if: steps.changed_files.outputs.any_changed == 'true'
+ run: |
+ echo "Detected changed files:"
+ echo "${{ steps.changed_files.outputs.all_changed_files }}"
+ echo "::set-output name=source_code_changed::true"
+
+ generate-matrix:
+ name: Generate build matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: set-matrix
+ name: Generate matrix
+ run: |
+ matrix_json=$(python3 ./.github/workflows/generate-matrix.py --platform=linux)
+ echo "::set-output name=matrix::${matrix_json}"
+
+ build:
+ needs: [ generate-matrix, detect_run ]
+ strategy:
+ matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
+ fail-fast: false
+ defaults:
+ run:
+ shell: bash -l {0}
+
+
+ runs-on: ${{ matrix.runs_on }}
+ name: ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+ container:
+ image: ghcr.io/${{ github.repository }}/build-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}:main
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+
+
+ - name: Create Build Environment
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ cmake -E make_directory build
+
+ - name: Configure CMake
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ QT_VERSION_MAJOR=$(echo ${{ matrix.qt_version }} | cut -d'.' -f1)
+
+ EXTRA_CMAKE_FLAGS="${EXTRA_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=ON"
+ # Disable ASAN for clang-11 - we are hitting some bugs in ASAN & generators
+ if [[ "${{ matrix.compiler }}" == "clang" && "${{ matrix.compiler_version }}" == "11" ]]; then
+ EXTRA_CMAKE_FLAGS="${EXTRA_CMAKE_FLAGS} -DQCORO_ENABLE_ASAN=OFF"
+ fi
+
+ cmake -B build \
+ -DCMAKE_BUILD_TYPE=$BUILD_TYPE \
+ -DUSE_QT_VERSION=$QT_VERSION_MAJOR \
+ -DQCORO_WITH_QTDBUS=${{ matrix.with_qtdbus }} \
+ -DQCORO_ENABLE_ASAN=ON \
+ ${EXTRA_CMAKE_FLAGS}
+
+ - name: Build
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ cmake --build build --config $BUILD_TYPE --parallel $(nproc) --verbose
+
+ - name: Test
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ cd build
+ QT_LOGGING_TO_CONSOLE=1 ctest -C $BUILD_TYPE \
+ --output-on-failure \
+ --verbose \
+ --output-junit ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload Test Results
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit Tests Results (${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }})
+ path: |
+ ${{ github.workspace }}/build/${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload build logs on failure
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && failure() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+ path: build/**
+
+ event_file:
+ name: "Event File"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: Event File
+ path: ${{ github.event_path }}
--- /dev/null
+name: MacOS CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+env:
+ BUILD_TYPE: Debug
+ QTEST_FUNCTION_TIMEOUT: 60000
+
+
+jobs:
+ detect_run:
+ name: Check changes
+ outputs: { "source_code_changed": "${{ steps.source_code_changed.outputs.source_code_changed}}" }
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: changed_files
+ uses: tj-actions/changed-files@v46
+ name: Get changed files
+ with:
+ files_ignore: |
+ docs/**
+ docker/**
+ requirements.txt
+ mkdocs.yml
+ README.md
+ .github/**
+ - id: source_code_changed
+ name: Check for source code changes
+ if: steps.changed_files.outputs.any_changed == 'true'
+ run: |
+ echo "Detected changed files:"
+ echo "${{ steps.changed_files.outputs.all_changed_files }}"
+ echo "::set-output name=source_code_changed::true"
+
+ generate-matrix:
+ name: Generate build matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: set-matrix
+ name: Generate matrix
+ run: |
+ matrix_json=$(python3 ./.github/workflows/generate-matrix.py --platform=macos)
+ echo "::set-output name=matrix::${matrix_json}"
+
+ build:
+ needs: [ generate-matrix, detect_run ]
+ strategy:
+ matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
+ fail-fast: false
+
+ runs-on: ${{ matrix.runs_on }}
+ name: ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+
+ - name: Install Qt
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ uses: ./.github/actions/install-qt
+ with:
+ qt_version: ${{ matrix.qt_version }}
+ qt_archives: ${{ matrix.qt_archives }}
+ qt_modules: ${{ matrix.qt_modules }}
+ platform: ${{ matrix.platform }}
+
+ - name: Create Build Environment
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ cmake -E make_directory ${{ github.workspace }}/build
+
+ - name: Configure CMake
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ QT_VERSION_MAJOR=$(echo ${{ matrix.qt_version }} | cut -d'.' -f1)
+
+ # Don't fiddle with dlls in Windows, it makes things complicated...
+ EXTRA_CMAKE_FLAGS="${EXTRA_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=ON"
+
+ cmake $GITHUB_WORKSPACE \
+ -DCMAKE_BUILD_TYPE=$BUILD_TYPE \
+ -DUSE_QT_VERSION=$QT_VERSION_MAJOR \
+ -DQCORO_WITH_QTDBUS=${{ matrix.with_qtdbus }} \
+ -DQCORO_ENABLE_ASAN=ON \
+ ${EXTRA_CMAKE_FLAGS}
+
+ - name: Build
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ cmake --build . --config $BUILD_TYPE --parallel $(nproc) --verbose
+
+ - name: Test
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ QT_LOGGING_TO_CONSOLE=1 ctest -C $BUILD_TYPE \
+ --output-on-failure \
+ --verbose \
+ --output-junit ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload Test Results
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit Tests Results (${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }})
+ path: |
+ ${{ github.workspace }}/build/${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload build logs on failure
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && failure() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+ path: build/**
+
+ event_file:
+ name: "Event File"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: Event File
+ path: ${{ github.event_path }}
--- /dev/null
+name: Windows CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+env:
+ BUILD_TYPE: Debug
+ QTEST_FUNCTION_TIMEOUT: 60000
+
+
+jobs:
+ detect_run:
+ name: Check changes
+ outputs: { "source_code_changed": "${{ steps.source_code_changed.outputs.source_code_changed}}" }
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: changed_files
+ uses: tj-actions/changed-files@v46
+ name: Get changed files
+ with:
+ files_ignore: |
+ docs/**
+ docker/**
+ requirements.txt
+ mkdocs.yml
+ README.md
+ .github/**
+ - id: source_code_changed
+ name: Check for source code changes
+ if: steps.changed_files.outputs.any_changed == 'true'
+ run: |
+ echo "Detected changed files:"
+ echo "${{ steps.changed_files.outputs.all_changed_files }}"
+ echo '::set-output name=source_code_changed::true'
+
+
+ generate-matrix:
+ name: Generate build matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: set-matrix
+ name: Generate matrix
+ run: |
+ matrix_json=$(python3 ./.github/workflows/generate-matrix.py --platform=windows)
+ echo "::set-output name=matrix::${matrix_json}"
+
+ build:
+ needs: [ generate-matrix, detect_run ]
+ strategy:
+ matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
+ fail-fast: false
+
+ runs-on: ${{ matrix.runs_on }}
+ name: ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+
+ steps:
+ - name: Checkout sources
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ uses: actions/checkout@v4
+
+ - name: Install Qt
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ uses: ./.github/actions/install-qt
+ with:
+ qt_version: ${{ matrix.qt_version }}
+ qt_archives: ${{ matrix.qt_archives }}
+ qt_modules: ${{ matrix.qt_modules }}
+ platform: ${{ matrix.platform }}
+
+ - name: Create Build Environment
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ run: |
+ cmake -E make_directory ${{ github.workspace }}/build
+
+ - name: Configure CMake
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ QT_VERSION_MAJOR=$(echo ${{ matrix.qt_version }} | cut -d'.' -f1)
+
+ if [[ "${{ matrix.compiler }}" == "clang-cl" ]]; then
+ # FIXME: allow ASAN with clang-cl
+ EXTRA_CMAKE_FLAGS="${EXTRA_CMAKE_FLAGS} -T ClangCL -DQCORO_ENABLE_ASAN=OFF"
+ fi
+ if [[ "${QT_VERSION_MAJOR}" == "5" ]]; then
+ EXTRA_CMAKE_FLAGS="${EXTRA_CMAKE_FLAGS} -DQCORO_ENABLE_ASAN=OFF"
+ fi
+
+ cmake $GITHUB_WORKSPACE \
+ -DCMAKE_BUILD_TYPE=$BUILD_TYPE \
+ -DUSE_QT_VERSION=$QT_VERSION_MAJOR \
+ -DQCORO_WITH_QTDBUS=${{ matrix.with_qtdbus }} \
+ -DQCORO_ENABLE_ASAN=ON \
+ ${EXTRA_CMAKE_FLAGS}
+
+ - name: Add ASAN DLL directory to PATH
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: pwsh
+ run: |
+ $installDir=(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath)
+ echo "Detected MSVC install dir: \"${installDir}\""
+ if ("${{ matrix.compiler}}" -eq "msvc") {
+ if (Test-Path -Path "${installDir}\VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt") {
+ $vctoolsVersion=(Get-Content -Path "${installDir}\VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt")
+ echo "Detected VCTools version: ${vctoolsVersion}"
+ } else {
+ echo "Failed to detect MSVC version"
+ exit 1
+ }
+ "${installDir}\VC\Tools\MSVC\${vctoolsVersion}\bin\HostX64\x64" >> $env:GITHUB_PATH
+ } else {
+ $clangVersion=((& "${installDir}\VC\Tools\Llvm\x64\bin\clang.exe" --version) | Select-String -Pattern "\d+.\d+.\d+" | % { $_.Matches } | % { $_.Value })
+ echo "Detected clang version: ${clangVersion}"
+ "${installDir}\VC\Tools\Llvm\x64\lib\clang\${clangVersion}\lib\windows" >> $env:GITHUB_PATH
+ }
+
+ - name: Build
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ cmake --build . --config $BUILD_TYPE --parallel $(nproc) --verbose
+
+ - name: Test
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' }}
+ shell: bash
+ working-directory: ${{ github.workspace }}/build
+ run: |
+ QT_LOGGING_TO_CONSOLE=1 ctest -C $BUILD_TYPE \
+ --output-on-failure \
+ --verbose \
+ --output-junit ${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload Test Results
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit Tests Results (${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }})
+ path: |
+ ${{ github.workspace }}/build/${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}.xml
+
+ - name: Upload build logs on failure
+ if: ${{ needs.detect_run.outputs.source_code_changed == 'true' && failure() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-${{ matrix.platform }}-${{ matrix.compiler_full }}-qt-${{ matrix.qt_version }}
+ path: build/**
+
+ event_file:
+ name: "Event File"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: Event File
+ path: ${{ github.event_path }}
--- /dev/null
+import json
+from argparse import ArgumentParser
+from typing import Any, Optional
+
+qt5_config = {
+ "archives": ["qtbase", "icu", "qtwebsockets", "qtdeclarative", "qtwebchannel", "qtlocation"],
+ "modules": ["qtwebengine"]
+}
+
+qt6_config = {
+ "archives": ["qtbase", "icu", "qtdeclarative"],
+ "modules": ["qtwebsockets", "qtwebengine", "qtwebchannel", "qtpositioning"]
+}
+
+qt = [
+ {
+ "version": "5.15.2",
+ **qt5_config
+ },
+ {
+ "version": "6.2.0",
+ **qt6_config
+ },
+ {
+ "version": "6.5.0",
+ **qt6_config
+ }
+]
+
+platforms = [
+ {
+ "name": "windows",
+ "compilers": [
+ {"name": "msvc"},
+ {"name": "clang-cl"}
+ ]
+ },
+ {
+ "name": "macos",
+ "compilers": [{"name": "apple-clang"}]
+ },
+ {
+ "name": "linux",
+ "compilers": [
+ {
+ "name": "gcc",
+ "versions": ["11", "12", "13", "14"]
+ },
+ {
+ "name": "clang",
+ "versions": ["15", "16", "17", "20", "dev"]
+ }
+ ]
+ }
+]
+
+
+output = {
+ "include": []
+}
+
+
+def get_os_for_platform(platform: str) -> str:
+ if platform == "windows":
+ return "windows-2022"
+ if platform == "linux":
+ return "ubuntu-20.04"
+ if platform == "macos":
+ return "macos-13"
+ raise RuntimeError(f"Invalid platform '{platform}'.")
+
+
+def get_base_image_for_compiler(compiler: str) -> Optional[str]:
+ if compiler == "gcc":
+ return "gcc"
+ if compiler == "clang":
+ return "debian"
+ return None
+
+
+def create_configuration(
+ qt: dict[str, Any], platform: str, compiler: str,
+ compiler_version: str = ""
+) -> dict[str, Any]:
+ return {
+ "qt_version": qt["version"],
+ "qt_modules": ' '.join(qt["modules"]),
+ "qt_archives": ' '.join(qt["archives"]),
+ "platform": platform,
+ "compiler": compiler,
+ "compiler_base_image": get_base_image_for_compiler(compiler),
+ "compiler_version": compiler_version,
+ "compiler_full": compiler if not compiler_version else f"{compiler}-{compiler_version}",
+ "runs_on": get_os_for_platform(platform),
+ "with_qtdbus": "OFF" if platform == "macos" else "ON"
+ }
+
+
+parser = ArgumentParser()
+parser.add_argument('--platform')
+args = parser.parse_args()
+
+filtered_platforms = list(filter(lambda p: p['name'] == args.platform, platforms))
+
+for qt_version in qt:
+ for platform in filtered_platforms:
+ for compiler in platform["compilers"]:
+ if "versions" in compiler:
+ for compiler_version in compiler["versions"]:
+ output["include"].append(
+ create_configuration(
+ qt_version, platform["name"], compiler["name"], compiler_version
+ )
+ )
+ else:
+ output["include"].append(
+ create_configuration(qt_version, platform["name"], compiler["name"]))
+
+print(json.dumps(output))
--- /dev/null
+name: Unit Test Results
+
+on:
+ workflow_run:
+ workflows: ["Build and Test"]
+ types:
+ - completed
+
+jobs:
+ unit-test-results:
+ name: Unit Test Results
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion != 'skipped'
+
+ steps:
+ - name: Download and Extract Artifacts
+ env:
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ run: |
+ mkdir -p artifacts && cd artifacts
+
+ artifacts_url=${{ github.event.workflow_run.artifacts_url }}
+
+ gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact
+ do
+ IFS=$'\t' read name url <<< "$artifact"
+ gh api $url > "$name.zip"
+ unzip -d "$name" "$name.zip"
+ done
+
+ - name: Publish Unit Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ commit: ${{ github.event.workflow_run.head_sha }}
+ event_file: artifacts/Event File/event.json
+ event_name: ${{ github.event.workflow_run.event }}
+ files: "artifacts/**/*.xml"
--- /dev/null
+name: Build and Deploy Documentation
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ detect_run:
+ name: Check changes
+ outputs: { "docs_changed": "${{ steps.docs_changed.outputs.docs_changed}}" }
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - id: changed_files
+ uses: tj-actions/changed-files@v46
+ name: Get changed files
+ with:
+ files: |
+ docs/**
+ requirements.txt
+ mkdocs.yml
+ README.md
+ .github/workflows/update-docs.yml
+ - id: docs_changed
+ name: Check for documentatio nchanges
+ if: steps.changed_files.outputs.any_changed == 'true'
+ run: |
+ echo "Detected changed files:"
+ echo "${{ steps.changed_files.outputs.all_changed_files }}"
+ echo "::set-output name=docs_changed::true"
+
+
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ needs: detect_run
+ if: ${{ needs.detect_run.outputs.docs_changed == 'true' }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+ - name: Install dependencies
+ run: |
+ python -m pip install -r requirements.txt
+ - name: Build
+ run: |
+ echo "{% extends \"base.html\" %}{% block analytics %}<!-- Matomo -->
+ <script type=\"text/javascript\">
+ var _paq = window._paq = window._paq || [];
+ _paq.push(['disableCookies']);
+ _paq.push(['trackPageView']);
+ _paq.push(['enableLinkTracking']);
+ (function() { var u='//analytics.dvratil.cz/';
+ _paq.push(['setTrackerUrl', u+'matomo.php']);
+ _paq.push(['setSiteId', '2']);
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+ })();</script><!-- End Matomo Code -->{% endblock %}" > docs/overrides/main.html
+ mkdocs build --verbose
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: site
+ path: site
+
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ if: ${{ github.ref == 'refs/heads/main' }}
+ environment:
+ name: cloudflare-pages
+ url: ${{ steps.deploy.outputs.url }}
+ needs: build
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: site
+ path: site
+ - name: Deploy to Cloudflare Pages
+ id: deploy
+ uses: cloudflare/pages-action@1
+ env:
+ CLOUDFLARE_ACCOUNT_ID: ${{ SECRETS.CLOUDFLARE_ACCOUNT_ID }}
+ with:
+ apiToken: ${{ SECRETS.CLOUDFLARE_PAGES_TOKEN }}
+ accountId: ${{ SECRETS.CLOUDFLARE_ACCOUNT_ID }}
+ projectName: ${{ vars.CLOUDFLARE_PAGES_NAME }}
+ directory: ./site
+ githubToken: ${{ SECRETS.GITHUB_TOKEN }}
+
--- /dev/null
+build/
+build-*/
+.*.swp
+.ccls-cache
+.cache
+compile_commands.json
+/.vs
+/.vscode
+/CMakeSettings.json
+
+# Python (from mkdocs)
+__pycache__
+venv
--- /dev/null
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: QCoro
+Upstream-Contact: Daniel Vrátil <dvratil@kde.org>
+Source: https://qcoro.github.io
+
+# Sample paragraph, commented out:
+#
+# Files: src/*
+# Copyright: $YEAR $NAME <$CONTACT>
+# License: ...
--- /dev/null
+cmake_minimum_required(VERSION 3.18.4)
+set(qcoro_VERSION 0.12.0)
+set(qcoro_SOVERSION 0)
+project(qcoro LANGUAGES CXX VERSION ${qcoro_VERSION})
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
+
+include(GNUInstallDirs)
+include(CTest)
+include(FeatureSummary)
+
+#-----------------------------------------------------------#
+# Options
+#-----------------------------------------------------------#
+
+
+# Allow QCORO_BUILD_TESTING to override BUILD_TESTING, but default to BUILD_TESTING if not set
+if (DEFINED QCORO_BUILD_TESTING)
+ set(BUILD_TESTING ${QCORO_BUILD_TESTING})
+endif()
+
+option(QCORO_BUILD_EXAMPLES "Build examples" ON)
+add_feature_info(Examples QCORO_BUILD_EXAMPLES "Build examples")
+option(QCORO_BUILD_TESTING "Build QCoro tests" ${BUILD_TESTING})
+add_feature_info(Testing QCORO_BUILD_TESTING "Build QCoro tests")
+option(QCORO_ENABLE_ASAN "Build with AddressSanitizer" OFF)
+add_feature_info(Asan QCORO_ENABLE_ASAN "Build with AddressSanitizer")
+option(QCORO_DISABLE_DEPRECATED_TASK_H "Disable deprecated task.h header" OFF)
+
+if(WIN32 OR APPLE OR ANDROID)
+ option(QCORO_WITH_QTDBUS "Build QtDBus support" OFF)
+else()
+ option(QCORO_WITH_QTDBUS "Build QtDBus support" ON)
+endif()
+add_feature_info(QtDBus QCORO_WITH_QTDBUS "Build QtDBus support")
+option(QCORO_WITH_QTNETWORK "Build QtNetwork support" ON)
+add_feature_info(QtNetwork QCORO_WITH_QTNETWORK "Build QtNetwork support")
+option(QCORO_WITH_QTWEBSOCKETS "Build QtWebSockets support" ON)
+add_feature_info(QtWebSockets QCORO_WITH_QTWEBSOCKETS "Build QtWebSockets support")
+option(QCORO_WITH_QTQUICK "Build QtQuick support" ON)
+add_feature_info(QtQuick QCORO_WITH_QTQUICK "Build QtQuick support")
+option(QCORO_WITH_QML "Build QML integration features" ON)
+add_feature_info(QtQml QCORO_WITH_QML "Build QML integration features")
+option(QCORO_WITH_QTTEST "Build QtTest support" ON)
+add_feature_info(QtTest QCORO_WITH_QTTEST "Build QtTest support")
+
+#-----------------------------------------------------------#
+# Dependencies
+#-----------------------------------------------------------#
+
+set(THREADS_PREFER_PTHREAD_FLAG TRUE)
+find_package(Threads REQUIRED)
+
+include(cmake/CheckAtomic.cmake)
+
+set(REQUIRED_QT_COMPONENTS Core)
+set(REQUIRED_QT5_COMPONENTS)
+set(REQUIRED_QT6_COMPONENTS)
+if (QCORO_WITH_QTDBUS)
+ list(APPEND REQUIRED_QT_COMPONENTS DBus)
+endif()
+if (QCORO_WITH_QTNETWORK)
+ list(APPEND REQUIRED_QT_COMPONENTS Network)
+endif()
+if (QCORO_WITH_QTWEBSOCKETS)
+ list(APPEND REQUIRED_QT_COMPONENTS WebSockets)
+endif()
+if (QCORO_WITH_QTQUICK)
+ list(APPEND REQUIRED_QT_COMPONENTS Gui Quick QuickPrivate)
+endif()
+if (QCORO_WITH_QML)
+ list(APPEND REQUIRED_QT_COMPONENTS Qml)
+ # Qt6 needs access to private API
+ list(APPEND REQUIRED_QT6_COMPONENTS QmlPrivate)
+endif()
+if (QCORO_WITH_QTTEST)
+ list(APPEND REQUIRED_QT_COMPONENTS Test)
+endif()
+if (QCORO_BUILD_EXAMPLES)
+ list(APPEND REQUIRED_QT_COMPONENTS Widgets Concurrent)
+endif()
+if (BUILD_TESTING)
+ list(APPEND REQUIRED_QT_COMPONENTS Test Concurrent)
+endif()
+
+set(MIN_REQUIRED_QT5_VERSION "5.12")
+set(MIN_REQUIRED_QT6_VERSION "6.2.0")
+
+include(cmake/QCoroFindQt.cmake)
+# Find Qt. If USE_QT_VERSION is not set, it will try to look for Qt6 first
+# and fallback to Qt5 otherwise.
+qcoro_find_qt(
+ QT_VERSION "${USE_QT_VERSION}"
+ COMPONENTS "${REQUIRED_QT_COMPONENTS}"
+ QT5_COMPONENTS "${REQUIRED_QT5_COMPONENTS}"
+ QT6_COMPONENTS "${REQUIRED_QT6_COMPONENTS}"
+ FOUND_VER_VAR QT_VERSION_MAJOR
+)
+
+configure_file(
+ ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in
+ ${CMAKE_CURRENT_BINARY_DIR}/config.h
+)
+
+#-----------------------------------------------------------#
+# Compiler Settings
+#-----------------------------------------------------------#
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_AUTOMOC ON)
+
+if (MSVC)
+ if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ # Explicitly enable exceptions support for clang-cl (it's only enabled by CMake when targeting the Windows-MSVC platform,
+ # see https://github.com/danvratil/qcoro/issues/90 for details)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc")
+ endif()
+else()
+ if (ANDROID)
+ include(DetectAndroidNDKVersion)
+ detectAndroidNDKVersion(NDK_VERSION)
+ if ("${NDK_VERSION}" VERSION_LESS 26)
+ # Android NDK < 26 ships mismatching versions of clang and libc++ - libc++ is older and doesn't have coroutine support
+ # which forces us to use coroutines from std::experimental. But the clang itself is newer and emits an error about
+ # std::experimental coroutines being deprecated in LLVM 14. This option is needed to suppress the error.
+ add_compile_options(-Wno-error=deprecated-experimental-coroutine)
+ endif()
+ endif()
+endif()
+
+if (QCORO_ENABLE_ASAN)
+ if (MSVC)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /INCREMENTAL:NO")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /INCREMENTAL:NO")
+ if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
+ else()
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address")
+ endif()
+ else()
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize-recover=address")
+ endif()
+endif()
+
+include(qcoro/QCoroMacros.cmake)
+qcoro_enable_coroutines()
+
+include(cmake/CodeCoverage.cmake)
+add_code_coverage()
+add_code_coverage_all_targets(EXCLUDE "${CMAKE_BINARY_DIR}" tests/utils/*)
+
+#-----------------------------------------------------------#
+# Definitions
+#-----------------------------------------------------------#
+
+# debug suffixes for qmake compatibility
+if(WIN32)
+ set(CMAKE_DEBUG_POSTFIX "d")
+elseif(APPLE)
+ set(CMAKE_DEBUG_POSTFIX "_debug")
+else()
+ set(CMAKE_DEBUG_POSTFIX "")
+endif()
+
+set(QCORO_TARGET_PREFIX "QCoro${QT_VERSION_MAJOR}")
+set(QCORO_INSTALL_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}/qcoro${QT_VERSION_MAJOR}")
+
+#-----------------------------------------------------------#
+# Sources
+#-----------------------------------------------------------#
+
+set(QCORO_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+add_subdirectory(qcoro)
+if (QCORO_BUILD_EXAMPLES)
+ add_subdirectory(examples)
+endif()
+if (QCORO_BUILD_TESTING)
+ add_subdirectory(tests)
+endif()
+
+#-----------------------------------------------------------#
+# Installation
+#-----------------------------------------------------------#
+
+include(CMakePackageConfigHelpers)
+
+install(FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/config.h
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/qcoro
+ COMPONENT Devel
+)
+
+configure_package_config_file(
+ "${CMAKE_CURRENT_SOURCE_DIR}/QCoroConfig.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}Config.cmake"
+ INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/QCoro${QT_VERSION_MAJOR}"
+ PATH_VARS CMAKE_INSTALL_INCLUDEDIR
+)
+
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}ConfigVersion.cmake"
+ VERSION ${qcoro_VERSION}
+ COMPATIBILITY SameMajorVersion
+)
+
+install(
+ FILES "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}Config.cmake"
+ "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}ConfigVersion.cmake"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/QCoro${QT_VERSION_MAJOR}"
+ COMPONENT Devel
+)
+
+#-----------------------------------------------------------#
+# Summary
+#-----------------------------------------------------------#
+
+feature_summary(FATAL_ON_MISSING_REQUIRED_PACKAGES WHAT ALL)
--- /dev/null
+MIT License
+
+Copyright (c) 2022 Daniel Vrátil <dvratil@kde.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+Copyright (c) <year> <owner>.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+GNU Free Documentation License
+Version 1.3, 3 November 2008
+
+Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. <http://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+0. PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.
+
+This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.
+
+We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.
+
+1. APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.
+
+A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.
+
+A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.
+
+The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.
+
+The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".
+
+Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.
+
+The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.
+
+The "publisher" means any person or entity that distributes copies of the Document to the public.
+
+A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.
+
+2. VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and you may publicly display copies.
+
+3. COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.
+
+If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.
+
+It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.
+
+4. MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:
+
+ A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
+ B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
+ C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
+ D. Preserve all the copyright notices of the Document.
+ E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
+ F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
+ G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice. H. Include an unaltered copy of this License.
+ I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
+ J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
+ K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
+ L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
+ M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
+ N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
+ O. Preserve any Warranty Disclaimers.
+
+If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.
+
+You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.
+
+5. COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements".
+
+6. COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.
+
+7. AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.
+
+8. TRANSLATION
+
+Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.
+
+9. TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, or distribute it is void, and will automatically terminate your rights under this License.
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, receipt of a copy of some or all of the same material does not give you any rights to use it.
+
+10. FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.
+
+Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. If the Document specifies that a proxy can decide which future versions of this License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Document.
+
+11. RELICENSING
+
+"Massive Multiauthor Collaboration Site" (or "MMC Site") means any World Wide Web server that publishes copyrightable works and also provides prominent facilities for anybody to edit those works. A public wiki that anybody can edit is an example of such a server. A "Massive Multiauthor Collaboration" (or "MMC") contained in the site means any set of copyrightable works thus published on the MMC site.
+
+"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 license published by Creative Commons Corporation, a not-for-profit corporation with a principal place of business in San Francisco, California, as well as future copyleft versions of that license published by that same organization.
+
+"Incorporate" means to publish or republish a Document, in whole or in part, as part of another Document.
+
+An MMC is "eligible for relicensing" if it is licensed under this License, and if all works that were first published under this License somewhere other than this MMC, and subsequently incorporated in whole or in part into the MMC, (1) had no cover texts or invariant sections, and (2) were thus incorporated prior to November 1, 2008.
+
+The operator of an MMC Site may republish an MMC contained in the site under CC-BY-SA on the same site at any time before August 1, 2009, provided the MMC is eligible for relicensing.
+
+ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:
+
+ Copyright (c) YEAR YOUR NAME. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this:
+
+ with the Invariant Sections being LIST THEIR TITLES, with the Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
+
+If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.
+
+If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.
--- /dev/null
+MIT License
+
+Copyright (c) 2021 Daniel Vrátil <dvratil@kde.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+if (CMAKE_VERSION VERSION_LESS 3.1.0)
+ message(FATAL_ERROR \"QCoro@QT_VERSION_MAJOR@ requires at least CMake version 3.1.0\")
+endif()
+
+if (NOT QCoro@QT_VERSION_MAJOR@_FIND_COMPONENTS)
+ set(QCoro@QT_VERSION_MAJOR@_NOT_FOUND_MESSAGE "The QCoro@QT_VERSION_MAJOR@ package requires at least one component")
+ set(QCoro@QT_VERSION_MAJOR@_FOUND FALSE)
+ return()
+endif()
+
+set(_QCoro_FIND_PARTS_REQUIRED)
+if (QCoro@QT_VERSION_MAJOR@_FIND_REQUIRED)
+ set(_QCoro_FIND_PARTS_REQUIRED REQUIRED)
+endif()
+set(_QCoro_FIND_PARTS_QUIET)
+if (QCoro@QT_VERSION_MAJOR@_FIND_QUIET)
+ set(_QCoro_FIND_PARTS_QUIET QUIET)
+endif()
+
+get_filename_component(_qcoro_install_prefix "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
+
+set(_QCoro_NOTFOUND_MESSAGE)
+
+foreach(module ${QCoro@QT_VERSION_MAJOR@_FIND_COMPONENTS})
+ find_package(QCoro@QT_VERSION_MAJOR@${module}
+ ${_QCoro_FIND_PARTS_QUIET}
+ ${_QCoro_FIND_PARTS_REQUIRED}
+ PATHS ${_qcoro_install_prefix} NO_DEFAULT_PATH
+ )
+ if (NOT QCoro@QT_VERSION_MAJOR@${module}_FOUND)
+ if (QCoro@QT_VERSION_MAJOR@_FIND_REQUIRED_${module})
+ set(_QCoro_NOTFOUND_MESSAGE "${_QCoro_NOTFOUND_MESSAGE}Failed to find QCoro component \"${module}\" config file at \"${_qcoro_install_prefix}\"\n")
+ elseif (NOT QCoro@QT_VERSION_MAJOR@_FIND_QUIETLY)
+ message(WARNING "Failed to find QCoro@QT_VERSION_MAJOR@ component \"${module}\" config file at \"${_qcoro_install_prefix}\"")
+ endif()
+ endif()
+endforeach()
+
+if (_QCoro_NOTFOUND_MESSAGE)
+ set(QCoro@QT_VERSION_MAJOR@_NOT_FOUND_MESSAGE "${_QCoro_NOTFOUND_MESSAGE}")
+ set(QCoro@QT_VERSION_MAJOR@_FOUND FALSE)
+endif()
+
--- /dev/null
+[](https://github.com/qcoro/qcoro/actions/workflows/build-linux.yml)
+[](https://github.com/qcoro/qcoro/actions/workflows/build-windows.yml)
+[](https://github.com/qcoro/qcoro/actions/workflows/build-macos.yml)
+[](https://github.com/qcoro/qcoro/actions/workflows/update-docs.yml)
+[](https://github.com/qcoro/qcoro/releases)
+
+
+
+
+# QCoro - Coroutines for Qt5 and Qt6
+
+The QCoro library provides set of tools to make use of C++20 coroutines with Qt.
+
+Take a look at the example below to see what an amazing thing coroutines are:
+```cpp
+QNetworkAccessManager networkAccessManager;
+// co_await the reply - the coroutine is suspended until the QNetworkReply is finished.
+// While the coroutine is suspended, *the Qt event loop runs as usual*.
+const QNetworkReply *reply = co_await networkAccessManager.get(url);
+// Once the reply is finished, your code resumes here as if nothing amazing has just happened ;-)
+const auto data = reply->readAll();
+```
+
+It requires a compiler with support for the couroutines TS, see [documentation](https://qcoro.dev/#supported-compilers) for a list of supported compilers and versions.
+
+## Documentation
+
+👉 📘 [Documentation](https://qcoro.dev/)
+
+## Supported Qt Types
+
+QCoro provides the tools necessary to make easy use of C++20 coroutines with Qt. The cornerstone of
+the library is `QCoro::Task<T>`, which represents an executed coroutine and allows the result of
+the coroutine to be asynchronously awaited by its caller. Additionally, QCoro provides a set of
+wrappers for common Qt types, such as `QTimer`, `QNetworkReply`, `QDBusPendingCall`, `QFuture`
+and others, that allow to `co_await` their asynchronous operations directly.
+
+Additionally, there's a magical `qCoro()` function that can wrap many native Qt functions and types
+to make them coroutine-friendly.
+
+Go check the [documentation](https://qcoro.dev/reference) for a full list of all supported
+features and Qt types.
+
+### `QDBusPendingCall`
+
+QCoro can wait for an asynchronous D-Bus call to finish. There's no need to use `QDBusPendingCallWatcher`
+with QCoro - just `co_await` the result instead. While co_awaiting, the Qt event loop runs as usual.
+
+```cpp
+QDBusInterface remoteServiceInterface{serviceName, objectPath, interface};
+const QDBusReply<bool> isReady = co_await remoteServiceInterface.asyncCall(QStringLiteral("isReady"));
+```
+
+📘 [Full documentation here](https://qcoro.dev/reference/dbus/qdbuspendingcall).
+
+### `QFuture`
+
+QFuture represents a result of an asynchronous task. Normally you have to use `QFutureWatcher` to get
+notified when the future is ready. With QCoro, you can just `co_await` it!
+
+```cpp
+const QFuture<int> task1 = QtConcurrent::run(....);
+const QFuture<int> task2 = QtConcurrent::run(....);
+
+const int a = co_await task1;
+const int b = co_await task2;
+
+co_return a + b;
+```
+
+📘 [Full documentation here](https://qcoro.dev/reference/core/qfuture).
+
+### `QNetworkReply`
+
+Doing network requests with Qt can be tedious - the signal/slot approach breaks the flow
+of your code. Chaining requests and error handling quickly become mess and your code is
+broken into numerous functions. But not with QCoro, where you can simply `co_await` the
+`QNetworkReply` to finish:
+
+```cpp
+QNetworkAccessManager qnam;
+QNetworkReply *reply = qnam.get(QStringLiteral("https://github.com/qcoro/qcoro"));
+const auto contents = co_await reply;
+reply->deleteLater();
+if (reply->error() != QNetworkReply::NoError) {
+ co_return handleError(reply);
+}
+
+const auto link = findLinkInReturnedHtmlCode(contents);
+reply = qnam.get(link);
+const auto data = co_await reply;
+reply->deleteLater();
+if (reply->error() != QNetworkReply::NoError) {
+ co_return handleError(reply);
+}
+ ...
+ ```
+
+📘 [Full documentation here](https://qcoro.dev/reference/network/qnetworkreply).
+
+### `QTimer`
+
+Maybe you want to delay executing your code for a second, maybe you want to execute some
+code in repeated interval. This becomes super-trivial with `co_await`:
+
+```cpp
+QTimer timer;
+timer.setInterval(1s);
+timer.start();
+
+for (int i = 1; i <= 100; ++i) {
+ co_await timer;
+ qDebug() << "Waiting for " << i << " seconds...";
+}
+
+qDebug() << "Done!";
+```
+
+📘 [Full documentation here](https://qcoro.dev/reference/core/qtimer).
+
+### `QIODevice`
+
+`QIODevice` is a base-class for many classes in Qt that allow data to be asynchronously
+written and read. How do you find out that there are data ready to be read? You could
+connect to `QIODevice::readyRead()` singal, or you could use QCoro and `co_await` the object:
+
+```cpp
+socket->write("PING");
+// Waiting for "pong"
+const auto data = co_await socket;
+co_return calculateLatency(data);
+```
+
+📘 [Full documentation here](https://qcoro.dev/reference/core/qiodevice).
+
+### ...and more!
+
+Go check the [full documentation](https://qcoro.dev) to learn more.
+
+## .then() continuations
+
+Sometimes it's not possible to use `co_await` to handle result of a coroutine - usually
+when interfacing with a 3rd party code that does not support coroutines. In those
+scenarios it's possible to chain a continuation callback to the coroutine which will
+get invoked asynchronously when the coroutine finishes.
+
+```cpp
+void regularFunction() {
+ someCoroutineReturningInt().then([](int result) {
+ // handle result
+ });
+}
+```
+
+The continuation callback can also be a coroutine and the result of the entire
+expression is Task<T> where T is the return type of the continuation. Thanks to
+that it's possible to `co_await` the entire chain, or chain multiple `.then()`
+continuations.
+
+📘 [Full documentation here](https://qcoro.dev/reference/coro/task).
+
+## Generators
+
+Generator is a coroutine that lazily produces multiple values. While there's
+nothing Qt-specific, QCoro provides the necessary tools for users to create
+custom generators in their Qt applications.
+
+QCoro provides API for both synchronous generators (`QCoro::Generator<T>`)
+and asynchronous generators (`QCoro::AsyncGenerator<T>`). Both generators provide
+container-like API: `begin()` and `end()` member functions that return iterator-like
+objects, which is well-known and established API and makes generators compatible
+with existing algorithms.
+
+```cpp
+QCoro::Generator<int> fibonacci() {
+ quint64 a = 0, b = 0;
+ Q_FOREVER {
+ co_yield b;
+ const auto tmp = b;
+ a = b;
+ b += tmp;
+ }
+}
+
+void printFib(quint64 max) {
+ for (auto fib : fibonacci()) {
+ if (fib > max) {
+ break;
+ }
+ std::cout << fib << std::endl;
+ }
+}
+```
+
+📘 [Full documentation here](https://qcoro.dev/reference/coro/generator).
+
+## License
+
+```text
+MIT License
+
+Copyright (c) 2022 Daniel Vrátil <dvratil@kde.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
--- /dev/null
+# SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+include(GenerateHeaders)
+include(GenerateExportHeader)
+include(GenerateModuleConfigFile)
+include(ECMGeneratePriFile)
+
+function(set_target_defaults target_name)
+ set(DEFAULT_QT_DEFINITIONS QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII QT_NO_URL_CAST_FROM_STRING QT_NO_CAST_FROM_BYTEARRAY QT_USE_STRINGBUILDER QT_NO_NARROWING_CONVERSIONS_IN_CONNECT QT_NO_KEYWORDS QT_NO_FOREACH)
+
+ get_target_property(target_type ${target_name} TYPE)
+ if (target_type STREQUAL "INTERFACE_LIBRARY")
+ # We can't set compile definitions for interface libraries as that would leak into user code
+ return()
+ endif()
+
+ target_compile_definitions(${target_name} PRIVATE ${DEFAULT_QT_DEFINITIONS})
+
+ if (NOT WIN32)
+ # strict iterators on MSVC only work when Qt itself is also built with them,
+ # which is not usually the case. Otherwise there are linking issues.
+ target_compile_definitions(${target_name} PRIVATE QT_STRICT_ITERATORS)
+ endif()
+
+ string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type_lowercase)
+ if ("${build_type_lowercase}" STREQUAL "debug")
+ if (MSVC)
+ target_compile_options(${target_name} PRIVATE /W4 /WX)
+ # Disable warning C5054: "operator '&': deprecated between enumerations of different types" caused by QtWidgets/qsizepolicy.h
+ # Disable warning C4127: "conditional expression is constant" caused by QtCore/qiterable.h
+ target_compile_options(${target_name} PRIVATE /wd5054 /wd4127)
+ if ("${QT_VERSION_MAJOR}" STREQUAL "6" AND "${Qt6_VERSION}" VERSION_GREATER_EQUAL "6.4.0" AND "${Qt6_VERSION}" VERSION_LESS "6.5.3")
+ # Disable warning C4702: "unreachable code" caused by QtTest/qtestcase.h - fixed in Qt 6.5.3
+ target_compile_options(${target_name} PRIVATE /wd4702)
+ endif()
+ else()
+ target_compile_options(${target_name} PRIVATE -Wall -Wextra -Werror -pedantic -Wno-language-extension-token)
+ endif()
+ endif()
+
+endfunction()
+
+function(add_qcoro_library)
+ function(prefix_libraries)
+ set(oneValueArgs PREFIX OUTPUT)
+ set(multiValueArgs LIBRARIES)
+ cmake_parse_arguments(prf "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ set(_libs)
+ foreach(libname ${prf_LIBRARIES})
+ if ("${libname}" MATCHES "PUBLIC|PRIVATE|INTERFACE")
+ list(APPEND _libs "${libname}")
+ else()
+ list(APPEND _libs "${prf_PREFIX}::${libname}")
+ endif()
+ endforeach()
+
+ set(${prf_OUTPUT} ${_libs} PARENT_SCOPE)
+ endfunction()
+
+ function(process_qmake_deps)
+ set(oneValueArgs PREFIX OUTPUT)
+ set(multiValueArgs LIBRARIES)
+ cmake_parse_arguments(pqd "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ set(_libs_priv FALSE)
+ set(_deps)
+ foreach (dep ${pqd_LIBRARIES})
+ if ("${dep}" MATCHES "PUBLIC|INTERFACE|public|interface")
+ set(_libs_priv FALSE)
+ continue()
+ elseif ("${dep}" MATCHES "PRIVATE|private")
+ set(_libs_priv TRUE)
+ continue()
+ endif()
+ if (NOT _libs_priv)
+ set(_deps "${_deps} ${pqd_PREFIX}${dep}")
+ endif()
+ endforeach()
+ set(${pqd_OUTPUT} ${_deps} PARENT_SCOPE)
+ endfunction()
+
+ set(params INTERFACE NO_CMAKE_CONFIG)
+ set(oneValueArgs NAME)
+ set(multiValueArgs SOURCES CAMELCASE_HEADERS HEADERS QCORO_LINK_LIBRARIES QT_LINK_LIBRARIES)
+
+ cmake_parse_arguments(LIB "${params}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ set(target_name "${QCORO_TARGET_PREFIX}${LIB_NAME}")
+ string(TOLOWER "${target_name}" target_name_lowercase)
+ set(target_interface)
+ set(target_include_interface "PUBLIC")
+ if (LIB_INTERFACE)
+ set(target_interface "INTERFACE")
+ set(target_include_interface "INTERFACE")
+ endif()
+
+ prefix_libraries(
+ PREFIX ${QCORO_TARGET_PREFIX}
+ LIBRARIES ${LIB_QCORO_LINK_LIBRARIES}
+ OUTPUT qcoro_LIBS
+ )
+
+ prefix_libraries(
+ PREFIX Qt${QT_VERSION_MAJOR}
+ LIBRARIES ${LIB_QT_LINK_LIBRARIES}
+ OUTPUT qt_LIBS
+ )
+
+ # TODO: How is it done in Qt?
+ # We want to export target QCoro5::Network but so far we are exporting
+ # QCoro5::QCoro5Network :shrug:
+ add_library(${target_name} ${target_interface})
+ add_library(${QCORO_TARGET_PREFIX}::${LIB_NAME} ALIAS ${target_name})
+ if (LIB_SOURCES)
+ target_sources(${target_name} PRIVATE ${LIB_SOURCES})
+ endif()
+
+ target_include_directories(
+ ${target_name}
+ ${target_include_interface} $<BUILD_INTERFACE:${QCORO_SOURCE_DIR}>
+ ${target_include_interface} $<BUILD_INTERFACE:${QCORO_SOURCE_DIR}/qcoro>
+ ${target_include_interface} $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ ${target_include_interface} $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+ ${target_include_interface} $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+ ${target_include_interface} $<INSTALL_INTERFACE:${QCORO_INSTALL_INCLUDEDIR}>
+ ${target_include_interface} $<INSTALL_INTERFACE:${QCORO_INSTALL_INCLUDEDIR}/qcoro>
+ ${target_include_interface} $<INSTALL_INTERFACE:${QCORO_INSTALL_INCLUDEDIR}/QCoro>
+ )
+
+ target_link_libraries(${target_name} ${qcoro_LIBS})
+ target_link_libraries(${target_name} ${qt_LIBS})
+
+ if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB AND NOT LIB_INTERFACE)
+ target_link_libraries(${target_name} PUBLIC atomic)
+ endif()
+
+ set_target_properties(
+ ${target_name}
+ PROPERTIES
+ EXPORT_NAME ${LIB_NAME}
+ )
+
+ set_target_defaults(${target_name})
+
+ if (NOT LIB_INTERFACE)
+ set_target_properties(
+ ${target_name}
+ PROPERTIES
+ WINDOWS_EXPORT_ALL_SYMBOLS 1
+ VERSION ${qcoro_VERSION}
+ SOVERSION ${qcoro_SOVERSION}
+ )
+ target_code_coverage(${target_name} AUTO)
+
+ else()
+ target_code_coverage(${target_name} AUTO INTERFACE)
+ endif()
+
+
+ generate_headers(
+ camelcase_HEADERS
+ HEADER_NAMES ${LIB_CAMELCASE_HEADERS}
+ OUTPUT_DIR QCoro
+ ORIGINAL_HEADERS_VAR source_HEADERS
+ )
+
+ if (NOT LIB_INTERFACE)
+ string(TOUPPER "qcoro${LIB_NAME}" export_name)
+ string(TOLOWER "${export_name}" export_file)
+ generate_export_header(
+ ${target_name}
+ BASE_NAME ${export_name}
+ )
+ endif()
+
+ if (NOT LIB_NO_CMAKE_CONFIG)
+ generate_cmake_module_config_file(
+ NAME ${LIB_NAME}
+ TARGET_NAME ${target_name}
+ QT_DEPENDENCIES ${LIB_QT_LINK_LIBRARIES}
+ QCORO_DEPENDENCIES ${LIB_QCORO_LINK_LIBRARIES}
+ )
+ endif()
+
+ string(TOLOWER "${LIB_QT_LINK_LIBRARIES}" lc_qt_link_libraries)
+ process_qmake_deps(
+ OUTPUT qmake_qt_deps
+ LIBRARIES ${lc_qt_link_libraries}
+ )
+
+ process_qmake_deps(
+ PREFIX QCoro
+ OUTPUT qmake_qcoro_deps
+ LIBRARIES ${LIB_QCORO_LINK_LIBRARIES}
+ )
+
+ set(egp_INTERFACE)
+ if (LIB_INTERFACE)
+ set(egp_INTERFACE "INTERFACE")
+ endif()
+
+ ecm_generate_pri_file(
+ ${egp_INTERFACE}
+ BASE_NAME QCoro${LIB_NAME}
+ LIB_NAME ${target_name}
+ VERSION ${qcoro_VERSION}
+ INCLUDE_INSTALL_DIRS ${QCORO_INSTALL_INCLUDEDIR}/qcoro;${QCORO_INSTALL_INCLUDEDIR}/QCoro
+ DEPS "${qmake_qt_deps} ${qmake_qcoro_deps}"
+ )
+
+ install(
+ TARGETS ${target_name}
+ EXPORT ${target_name}Targets
+ )
+ install(
+ FILES ${source_HEADERS}
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/qcoro/
+ COMPONENT Devel
+ )
+ foreach(lib_header ${LIB_HEADERS})
+ get_filename_component(header_prefix_dir ${lib_header} DIRECTORY)
+ install(
+ FILES ${lib_header}
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/qcoro/${header_prefix_dir}
+ COMPONENT Devel
+ )
+ endforeach()
+ install(
+ FILES ${camelcase_HEADERS}
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/QCoro/
+ COMPONENT Devel
+ )
+ if (NOT LIB_INTERFACE)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${export_file}_export.h
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/qcoro
+ COMPONENT Devel
+ )
+ endif()
+
+ install(
+ FILES "${CMAKE_CURRENT_BINARY_DIR}/${target_name}Config.cmake"
+ "${CMAKE_CURRENT_BINARY_DIR}/${target_name}ConfigVersion.cmake"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${target_name}"
+ COMPONENT Devel
+ )
+ install(
+ EXPORT ${target_name}Targets
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${target_name}"
+ FILE "${target_name}Targets.cmake"
+ NAMESPACE ${QCORO_TARGET_PREFIX}::
+ COMPONENT Devel
+ )
+ install(
+ FILES "${CMAKE_CURRENT_BINARY_DIR}/qt_QCoro${LIB_NAME}.pri"
+ DESTINATION "${ECM_MKSPECS_INSTALL_DIR}"
+ COMPONENT Devel
+ )
+endfunction()
--- /dev/null
+# std::atomic may need libatomic to function correctly.
+
+INCLUDE(CheckCXXSourceCompiles)
+INCLUDE(CheckLibraryExists)
+
+# Sometimes linking against libatomic is required for atomic ops, if
+# the platform doesn't support lock-free atomics.
+
+function(check_working_cxx_atomics varname)
+ set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
+ set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++20")
+ CHECK_CXX_SOURCE_COMPILES("
+#include <atomic>
+std::atomic<int> x;
+std::atomic<short> y;
+std::atomic<char> z;
+int main() {
+ ++z;
+ ++y;
+ return ++x;
+}
+" ${varname})
+ set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
+endfunction(check_working_cxx_atomics)
+
+# Check for (non-64-bit) atomic operations.
+if(MSVC)
+ set(HAVE_CXX_ATOMICS_WITHOUT_LIB True)
+else()
+ # First check if atomics work without the library.
+ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ # If not, check if the library exists, and atomics work with it.
+ if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic")
+ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB)
+ if (NOT HAVE_CXX_ATOMICS_WITH_LIB)
+ message(FATAL_ERROR "Host compiler must support std::atomic!")
+ endif()
+ endif()
+endif()
--- /dev/null
+#
+# Copyright (C) 2018-2020 by George Cave - gcave@stablecoder.ca
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# USAGE: To enable any code coverage instrumentation/targets, the single CMake
+# option of `CODE_COVERAGE` needs to be set to 'ON', either by GUI, ccmake, or
+# on the command line.
+#
+# From this point, there are two primary methods for adding instrumentation to
+# targets: 1 - A blanket instrumentation by calling `add_code_coverage()`, where
+# all targets in that directory and all subdirectories are automatically
+# instrumented. 2 - Per-target instrumentation by calling
+# `target_code_coverage(<TARGET_NAME>)`, where the target is given and thus only
+# that target is instrumented. This applies to both libraries and executables.
+#
+# To add coverage targets, such as calling `make ccov` to generate the actual
+# coverage information for perusal or consumption, call
+# `target_code_coverage(<TARGET_NAME>)` on an *executable* target.
+#
+# Example 1: All targets instrumented
+#
+# In this case, the coverage information reported will will be that of the
+# `theLib` library target and `theExe` executable.
+#
+# 1a: Via global command
+#
+# ~~~
+# add_code_coverage() # Adds instrumentation to all targets
+#
+# add_library(theLib lib.cpp)
+#
+# add_executable(theExe main.cpp)
+# target_link_libraries(theExe PRIVATE theLib)
+# target_code_coverage(theExe) # As an executable target, adds the 'ccov-theExe' target (instrumentation already added via global anyways) for generating code coverage reports.
+# ~~~
+#
+# 1b: Via target commands
+#
+# ~~~
+# add_library(theLib lib.cpp)
+# target_code_coverage(theLib) # As a library target, adds coverage instrumentation but no targets.
+#
+# add_executable(theExe main.cpp)
+# target_link_libraries(theExe PRIVATE theLib)
+# target_code_coverage(theExe) # As an executable target, adds the 'ccov-theExe' target and instrumentation for generating code coverage reports.
+# ~~~
+#
+# Example 2: Target instrumented, but with regex pattern of files to be excluded
+# from report
+#
+# ~~~
+# add_executable(theExe main.cpp non_covered.cpp)
+# target_code_coverage(theExe EXCLUDE non_covered.cpp test/*) # As an executable target, the reports will exclude the non-covered.cpp file, and any files in a test/ folder.
+# ~~~
+#
+# Example 3: Target added to the 'ccov' and 'ccov-all' targets
+#
+# ~~~
+# add_code_coverage_all_targets(EXCLUDE test/*) # Adds the 'ccov-all' target set and sets it to exclude all files in test/ folders.
+#
+# add_executable(theExe main.cpp non_covered.cpp)
+# target_code_coverage(theExe AUTO ALL EXCLUDE non_covered.cpp test/*) # As an executable target, adds to the 'ccov' and ccov-all' targets, and the reports will exclude the non-covered.cpp file, and any files in a test/ folder.
+# ~~~
+
+# Options
+option(
+ CODE_COVERAGE
+ "Builds targets with code coverage instrumentation. (Requires GCC or Clang)"
+ OFF)
+
+# Programs
+find_program(LLVM_COV_PATH llvm-cov)
+find_program(LLVM_PROFDATA_PATH llvm-profdata)
+find_program(LCOV_PATH lcov)
+find_program(GENHTML_PATH genhtml)
+# Hide behind the 'advanced' mode flag for GUI/ccmake
+mark_as_advanced(FORCE LLVM_COV_PATH LLVM_PROFDATA_PATH LCOV_PATH GENHTML_PATH)
+
+# Variables
+set(CMAKE_COVERAGE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/ccov)
+set_property(GLOBAL PROPERTY JOB_POOLS ccov_serial_pool=1)
+
+# Common initialization/checks
+if(CODE_COVERAGE AND NOT CODE_COVERAGE_ADDED)
+ set(CODE_COVERAGE_ADDED ON)
+
+ # Common Targets
+ add_custom_target(
+ ccov-preprocessing
+ COMMAND ${CMAKE_COMMAND} -E make_directory
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}
+ DEPENDS ccov-clean)
+
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+ # Messages
+ message(STATUS "Building with llvm Code Coverage Tools")
+
+ if(NOT LLVM_COV_PATH)
+ message(FATAL_ERROR "llvm-cov not found! Aborting.")
+ else()
+ # Version number checking for 'EXCLUDE' compatibility
+ execute_process(COMMAND ${LLVM_COV_PATH} --version
+ OUTPUT_VARIABLE LLVM_COV_VERSION_CALL_OUTPUT)
+ string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" LLVM_COV_VERSION
+ ${LLVM_COV_VERSION_CALL_OUTPUT})
+
+ if(LLVM_COV_VERSION VERSION_LESS "7.0.0")
+ message(
+ WARNING
+ "target_code_coverage()/add_code_coverage_all_targets() 'EXCLUDE' option only available on llvm-cov >= 7.0.0"
+ )
+ endif()
+ endif()
+
+ # Targets
+ if(${CMAKE_VERSION} VERSION_LESS "3.17.0")
+ add_custom_target(
+ ccov-clean
+ COMMAND ${CMAKE_COMMAND} -E remove -f
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list
+ COMMAND ${CMAKE_COMMAND} -E remove -f
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list)
+ else()
+ add_custom_target(
+ ccov-clean
+ COMMAND ${CMAKE_COMMAND} -E rm -f
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list
+ COMMAND ${CMAKE_COMMAND} -E rm -f
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list)
+ endif()
+
+ # Used to get the shared object file list before doing the main all-
+ # processing
+ add_custom_target(
+ ccov-libs
+ COMMAND ;
+ COMMENT "libs ready for coverage report.")
+
+ elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
+ "GNU")
+ # Messages
+ message(STATUS "Building with lcov Code Coverage Tools")
+
+ if(CMAKE_BUILD_TYPE)
+ string(TOUPPER ${CMAKE_BUILD_TYPE} upper_build_type)
+ if(NOT ${upper_build_type} STREQUAL "DEBUG")
+ message(
+ WARNING
+ "Code coverage results with an optimized (non-Debug) build may be misleading"
+ )
+ endif()
+ else()
+ message(
+ WARNING
+ "Code coverage results with an optimized (non-Debug) build may be misleading"
+ )
+ endif()
+ if(NOT LCOV_PATH)
+ message(FATAL_ERROR "lcov not found! Aborting...")
+ endif()
+ if(NOT GENHTML_PATH)
+ message(FATAL_ERROR "genhtml not found! Aborting...")
+ endif()
+
+ # Targets
+ add_custom_target(ccov-clean COMMAND ${LCOV_PATH} --directory
+ ${CMAKE_BINARY_DIR} --zerocounters)
+
+ else()
+ message(FATAL_ERROR "Code coverage requires Clang or GCC. Aborting.")
+ endif()
+endif()
+
+# Adds code coverage instrumentation to a library, or instrumentation/targets
+# for an executable target.
+# ~~~
+# EXECUTABLE ADDED TARGETS:
+# GCOV/LCOV:
+# ccov : Generates HTML code coverage report for every target added with 'AUTO' parameter.
+# ccov-${TARGET_NAME} : Generates HTML code coverage report for the associated named target.
+# ccov-all : Generates HTML code coverage report, merging every target added with 'ALL' parameter into a single detailed report.
+#
+# LLVM-COV:
+# ccov : Generates HTML code coverage report for every target added with 'AUTO' parameter.
+# ccov-report : Generates HTML code coverage report for every target added with 'AUTO' parameter.
+# ccov-${TARGET_NAME} : Generates HTML code coverage report.
+# ccov-report-${TARGET_NAME} : Prints to command line summary per-file coverage information.
+# ccov-export-${TARGET_NAME} : Exports the coverage report to a JSON file.
+# ccov-show-${TARGET_NAME} : Prints to command line detailed per-line coverage information.
+# ccov-all : Generates HTML code coverage report, merging every target added with 'ALL' parameter into a single detailed report.
+# ccov-all-report : Prints summary per-file coverage information for every target added with ALL' parameter to the command line.
+# ccov-all-export : Exports the coverage report to a JSON file.
+#
+# Required:
+# TARGET_NAME - Name of the target to generate code coverage for.
+# Optional:
+# PUBLIC - Sets the visibility for added compile options to targets to PUBLIC instead of the default of PRIVATE.
+# INTERFACE - Sets the visibility for added compile options to targets to INTERFACE instead of the default of PRIVATE.
+# PLAIN - Do not set any target visibility (backward compatibility with old cmake projects)
+# AUTO - Adds the target to the 'ccov' target so that it can be run in a batch with others easily. Effective on executable targets.
+# ALL - Adds the target to the 'ccov-all' and 'ccov-all-report' targets, which merge several executable targets coverage data to a single report. Effective on executable targets.
+# EXTERNAL - For GCC's lcov, allows the profiling of 'external' files from the processing directory
+# COVERAGE_TARGET_NAME - For executables ONLY, changes the outgoing target name so instead of `ccov-${TARGET_NAME}` it becomes `ccov-${COVERAGE_TARGET_NAME}`.
+# EXCLUDE <PATTERNS> - Excludes files of the patterns provided from coverage. Note that GCC/lcov excludes by glob pattern, and clang/LLVM excludes via regex! **These do not copy to the 'all' targets.**
+# OBJECTS <TARGETS> - For executables ONLY, if the provided targets are shared libraries, adds coverage information to the output
+# ARGS <ARGUMENTS> - For executables ONLY, appends the given arguments to the associated ccov-* executable call
+# ~~~
+function(target_code_coverage TARGET_NAME)
+ # Argument parsing
+ set(options AUTO ALL EXTERNAL PUBLIC INTERFACE PLAIN)
+ set(single_value_keywords COVERAGE_TARGET_NAME)
+ set(multi_value_keywords EXCLUDE OBJECTS ARGS)
+ cmake_parse_arguments(
+ target_code_coverage "${options}" "${single_value_keywords}"
+ "${multi_value_keywords}" ${ARGN})
+
+ # Set the visibility of target functions to PUBLIC, INTERFACE or default to
+ # PRIVATE.
+ if(target_code_coverage_PUBLIC)
+ set(TARGET_VISIBILITY PUBLIC)
+ set(TARGET_LINK_VISIBILITY PUBLIC)
+ elseif(target_code_coverage_INTERFACE)
+ set(TARGET_VISIBILITY INTERFACE)
+ set(TARGET_LINK_VISIBILITY INTERFACE)
+ elseif(target_code_coverage_PLAIN)
+ set(TARGET_VISIBILITY PUBLIC)
+ set(TARGET_LINK_VISIBILITY)
+ else()
+ set(TARGET_VISIBILITY PRIVATE)
+ set(TARGET_LINK_VISIBILITY PRIVATE)
+ endif()
+
+ if(NOT target_code_coverage_COVERAGE_TARGET_NAME)
+ # If a specific name was given, use that instead.
+ set(target_code_coverage_COVERAGE_TARGET_NAME ${TARGET_NAME})
+ endif()
+
+ if(CODE_COVERAGE)
+
+ # Add code coverage instrumentation to the target's linker command
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+ target_compile_options(${TARGET_NAME} ${TARGET_VISIBILITY}
+ -fprofile-instr-generate -fcoverage-mapping)
+ target_link_options(${TARGET_NAME} ${TARGET_VISIBILITY}
+ -fprofile-instr-generate -fcoverage-mapping)
+ elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
+ "GNU")
+ target_compile_options(${TARGET_NAME} ${TARGET_VISIBILITY} -fprofile-arcs
+ -ftest-coverage)
+ target_link_libraries(${TARGET_NAME} ${TARGET_LINK_VISIBILITY} gcov)
+ endif()
+
+ # Targets
+ get_target_property(target_type ${TARGET_NAME} TYPE)
+
+ # Add shared library to processing for 'all' targets
+ if(target_type STREQUAL "SHARED_LIBRARY" AND target_code_coverage_ALL)
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+ add_custom_target(
+ ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${CMAKE_COMMAND} -E echo "-object=$<TARGET_FILE:${TARGET_NAME}>" >>
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list
+ DEPENDS ccov-preprocessing ${TARGET_NAME})
+
+ if(NOT TARGET ccov-libs)
+ message(
+ FATAL_ERROR
+ "Calling target_code_coverage with 'ALL' must be after a call to 'add_code_coverage_all_targets'."
+ )
+ endif()
+
+ add_dependencies(ccov-libs
+ ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME})
+ endif()
+ endif()
+
+ # For executables add targets to run and produce output
+ if(target_type STREQUAL "EXECUTABLE")
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+
+ # If there are shared objects to also work with, generate the string to
+ # add them here
+ foreach(SO_TARGET ${target_code_coverage_OBJECTS})
+ # Check to see if the target is a shared object
+ if(TARGET ${SO_TARGET})
+ get_target_property(SO_TARGET_TYPE ${SO_TARGET} TYPE)
+ if(${SO_TARGET_TYPE} STREQUAL "SHARED_LIBRARY")
+ set(SO_OBJECTS ${SO_OBJECTS} -object=$<TARGET_FILE:${SO_TARGET}>)
+ endif()
+ endif()
+ endforeach()
+
+ # Run the executable, generating raw profile data Make the run data
+ # available for further processing. Separated to allow Windows to run
+ # this target serially.
+ add_custom_target(
+ ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${CMAKE_COMMAND} -E env
+ LLVM_PROFILE_FILE=${target_code_coverage_COVERAGE_TARGET_NAME}.profraw
+ $<TARGET_FILE:${TARGET_NAME}> ${target_code_coverage_ARGS}
+ COMMAND
+ ${CMAKE_COMMAND} -E echo "-object=$<TARGET_FILE:${TARGET_NAME}>"
+ ${SO_OBJECTS} >> ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list
+ COMMAND
+ ${CMAKE_COMMAND} -E echo
+ "${CMAKE_CURRENT_BINARY_DIR}/${target_code_coverage_COVERAGE_TARGET_NAME}.profraw"
+ >> ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list
+ JOB_POOL ccov_serial_pool
+ DEPENDS ccov-preprocessing ccov-libs ${TARGET_NAME})
+
+ # Merge the generated profile data so llvm-cov can process it
+ add_custom_target(
+ ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${LLVM_PROFDATA_PATH} merge -sparse
+ ${target_code_coverage_COVERAGE_TARGET_NAME}.profraw -o
+ ${target_code_coverage_COVERAGE_TARGET_NAME}.profdata
+ DEPENDS ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ # Ignore regex only works on LLVM >= 7
+ if(LLVM_COV_VERSION VERSION_GREATER_EQUAL "7.0.0")
+ foreach(EXCLUDE_ITEM ${target_code_coverage_EXCLUDE})
+ set(EXCLUDE_REGEX ${EXCLUDE_REGEX}
+ -ignore-filename-regex='${EXCLUDE_ITEM}')
+ endforeach()
+ endif()
+
+ # Print out details of the coverage information to the command line
+ add_custom_target(
+ ccov-show-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${LLVM_COV_PATH} show $<TARGET_FILE:${TARGET_NAME}> ${SO_OBJECTS}
+ -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata
+ -show-line-counts-or-regions ${EXCLUDE_REGEX}
+ DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ # Print out a summary of the coverage information to the command line
+ add_custom_target(
+ ccov-report-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${LLVM_COV_PATH} report $<TARGET_FILE:${TARGET_NAME}> ${SO_OBJECTS}
+ -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata
+ ${EXCLUDE_REGEX}
+ DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ # Export coverage information so continuous integration tools (e.g.
+ # Jenkins) can consume it
+ add_custom_target(
+ ccov-export-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${LLVM_COV_PATH} export $<TARGET_FILE:${TARGET_NAME}> ${SO_OBJECTS}
+ -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata
+ -format="text" ${EXCLUDE_REGEX} >
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}.json
+ DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ # Generates HTML output of the coverage information for perusal
+ add_custom_target(
+ ccov-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${LLVM_COV_PATH} show $<TARGET_FILE:${TARGET_NAME}> ${SO_OBJECTS}
+ -instr-profile=${target_code_coverage_COVERAGE_TARGET_NAME}.profdata
+ -show-line-counts-or-regions
+ -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}
+ -format="html" ${EXCLUDE_REGEX}
+ DEPENDS ccov-processing-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
+ "GNU")
+ set(COVERAGE_INFO
+ "${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}.info"
+ )
+
+ # Run the executable, generating coverage information
+ add_custom_target(
+ ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND $<TARGET_FILE:${TARGET_NAME}> ${target_code_coverage_ARGS}
+ DEPENDS ccov-preprocessing ${TARGET_NAME})
+
+ # Generate exclusion string for use
+ foreach(EXCLUDE_ITEM ${target_code_coverage_EXCLUDE})
+ set(EXCLUDE_REGEX ${EXCLUDE_REGEX} --remove ${COVERAGE_INFO}
+ '${EXCLUDE_ITEM}')
+ endforeach()
+
+ if(EXCLUDE_REGEX)
+ set(EXCLUDE_COMMAND ${LCOV_PATH} ${EXCLUDE_REGEX} --output-file
+ ${COVERAGE_INFO})
+ else()
+ set(EXCLUDE_COMMAND ;)
+ endif()
+
+ if(NOT ${target_code_coverage_EXTERNAL})
+ set(EXTERNAL_OPTION --no-external)
+ endif()
+
+ # Capture coverage data
+ if(${CMAKE_VERSION} VERSION_LESS "3.17.0")
+ add_custom_target(
+ ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND ${CMAKE_COMMAND} -E remove -f ${COVERAGE_INFO}
+ COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters
+ COMMAND $<TARGET_FILE:${TARGET_NAME}> ${target_code_coverage_ARGS}
+ COMMAND
+ ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory
+ ${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file
+ ${COVERAGE_INFO}
+ COMMAND ${EXCLUDE_COMMAND}
+ DEPENDS ccov-preprocessing ${TARGET_NAME})
+ else()
+ add_custom_target(
+ ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND ${CMAKE_COMMAND} -E rm -f ${COVERAGE_INFO}
+ COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters
+ COMMAND $<TARGET_FILE:${TARGET_NAME}> ${target_code_coverage_ARGS}
+ COMMAND
+ ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --base-directory
+ ${CMAKE_SOURCE_DIR} --capture ${EXTERNAL_OPTION} --output-file
+ ${COVERAGE_INFO}
+ COMMAND ${EXCLUDE_COMMAND}
+ DEPENDS ccov-preprocessing ${TARGET_NAME})
+ endif()
+
+ # Generates HTML output of the coverage information for perusal
+ add_custom_target(
+ ccov-${target_code_coverage_COVERAGE_TARGET_NAME}
+ COMMAND
+ ${GENHTML_PATH} -o
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}
+ ${COVERAGE_INFO}
+ DEPENDS ccov-capture-${target_code_coverage_COVERAGE_TARGET_NAME})
+ endif()
+
+ add_custom_command(
+ TARGET ccov-${target_code_coverage_COVERAGE_TARGET_NAME}
+ POST_BUILD
+ COMMAND ;
+ COMMENT
+ "Open ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/${target_code_coverage_COVERAGE_TARGET_NAME}/index.html in your browser to view the coverage report."
+ )
+
+ # AUTO
+ if(target_code_coverage_AUTO)
+ if(NOT TARGET ccov)
+ add_custom_target(ccov)
+ endif()
+ add_dependencies(ccov ccov-${target_code_coverage_COVERAGE_TARGET_NAME})
+
+ if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU" AND NOT CMAKE_CXX_COMPILER_ID
+ MATCHES "GNU")
+ if(NOT TARGET ccov-report)
+ add_custom_target(ccov-report)
+ endif()
+ add_dependencies(
+ ccov-report
+ ccov-report-${target_code_coverage_COVERAGE_TARGET_NAME})
+ endif()
+ endif()
+
+ # ALL
+ if(target_code_coverage_ALL)
+ if(NOT TARGET ccov-all-processing)
+ message(
+ FATAL_ERROR
+ "Calling target_code_coverage with 'ALL' must be after a call to 'add_code_coverage_all_targets'."
+ )
+ endif()
+
+ add_dependencies(ccov-all-processing
+ ccov-run-${target_code_coverage_COVERAGE_TARGET_NAME})
+ endif()
+ endif()
+ endif()
+endfunction()
+
+# Adds code coverage instrumentation to all targets in the current directory and
+# any subdirectories. To add coverage instrumentation to only specific targets,
+# use `target_code_coverage`.
+function(add_code_coverage)
+ if(CODE_COVERAGE)
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+ add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
+ add_link_options(-fprofile-instr-generate -fcoverage-mapping)
+ elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
+ "GNU")
+ add_compile_options(-fprofile-arcs -ftest-coverage)
+ link_libraries(gcov)
+ endif()
+ endif()
+endfunction()
+
+# Adds the 'ccov-all' type targets that calls all targets added via
+# `target_code_coverage` with the `ALL` parameter, but merges all the coverage
+# data from them into a single large report instead of the numerous smaller
+# reports. Also adds the ccov-all-capture Generates an all-merged.info file, for
+# use with coverage dashboards (e.g. codecov.io, coveralls).
+# ~~~
+# Optional:
+# EXCLUDE <PATTERNS> - Excludes files of the patterns provided from coverage. Note that GCC/lcov excludes by glob pattern, and clang/LLVM excludes via regex!
+# ~~~
+function(add_code_coverage_all_targets)
+ # Argument parsing
+ set(multi_value_keywords EXCLUDE)
+ cmake_parse_arguments(add_code_coverage_all_targets "" ""
+ "${multi_value_keywords}" ${ARGN})
+
+ if(CODE_COVERAGE)
+ if(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
+ OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
+
+ # Merge the profile data for all of the run executables
+ if(WIN32)
+ add_custom_target(
+ ccov-all-processing
+ COMMAND
+ powershell -Command $$FILELIST = Get-Content
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list\; llvm-profdata.exe
+ merge -o ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ -sparse $$FILELIST)
+ else()
+ add_custom_target(
+ ccov-all-processing
+ COMMAND
+ ${LLVM_PROFDATA_PATH} merge -o
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata -sparse `cat
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/profraw.list`)
+ endif()
+
+ # Regex exclude only available for LLVM >= 7
+ if(LLVM_COV_VERSION VERSION_GREATER_EQUAL "7.0.0")
+ foreach(EXCLUDE_ITEM ${add_code_coverage_all_targets_EXCLUDE})
+ set(EXCLUDE_REGEX ${EXCLUDE_REGEX}
+ -ignore-filename-regex='${EXCLUDE_ITEM}')
+ endforeach()
+ endif()
+
+ # Print summary of the code coverage information to the command line
+ if(WIN32)
+ add_custom_target(
+ ccov-all-report
+ COMMAND
+ powershell -Command $$FILELIST = Get-Content
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list\; llvm-cov.exe
+ report $$FILELIST
+ -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ ${EXCLUDE_REGEX}
+ DEPENDS ccov-all-processing)
+ else()
+ add_custom_target(
+ ccov-all-report
+ COMMAND
+ ${LLVM_COV_PATH} report `cat
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list`
+ -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ ${EXCLUDE_REGEX}
+ DEPENDS ccov-all-processing)
+ endif()
+
+ # Export coverage information so continuous integration tools (e.g.
+ # Jenkins) can consume it
+ add_custom_target(
+ ccov-all-export
+ COMMAND
+ ${LLVM_COV_PATH} export `cat
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list`
+ -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ -format="text" ${EXCLUDE_REGEX} >
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/coverage.json
+ DEPENDS ccov-all-processing)
+
+ # Generate HTML output of all added targets for perusal
+ if(WIN32)
+ add_custom_target(
+ ccov-all
+ COMMAND
+ powershell -Command $$FILELIST = Get-Content
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list\; llvm-cov.exe show
+ $$FILELIST
+ -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ -show-line-counts-or-regions
+ -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged
+ -format="html" ${EXCLUDE_REGEX}
+ DEPENDS ccov-all-processing)
+ else()
+ add_custom_target(
+ ccov-all
+ COMMAND
+ ${LLVM_COV_PATH} show `cat
+ ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/binaries.list`
+ -instr-profile=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.profdata
+ -show-line-counts-or-regions
+ -output-dir=${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged
+ -format="html" ${EXCLUDE_REGEX}
+ DEPENDS ccov-all-processing)
+ endif()
+
+ elseif(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES
+ "GNU")
+ set(COVERAGE_INFO "${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged.info")
+
+ # Nothing required for gcov
+ add_custom_target(ccov-all-processing COMMAND ;)
+
+ # Exclusion regex string creation
+ set(EXCLUDE_REGEX)
+ foreach(EXCLUDE_ITEM ${add_code_coverage_all_targets_EXCLUDE})
+ set(EXCLUDE_REGEX ${EXCLUDE_REGEX} --remove ${COVERAGE_INFO}
+ '${EXCLUDE_ITEM}')
+ endforeach()
+
+ if(EXCLUDE_REGEX)
+ set(EXCLUDE_COMMAND ${LCOV_PATH} ${EXCLUDE_REGEX} --output-file
+ ${COVERAGE_INFO})
+ else()
+ set(EXCLUDE_COMMAND ;)
+ endif()
+
+ # Capture coverage data
+ if(${CMAKE_VERSION} VERSION_LESS "3.17.0")
+ add_custom_target(
+ ccov-all-capture
+ COMMAND ${CMAKE_COMMAND} -E remove -f ${COVERAGE_INFO}
+ COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture
+ --output-file ${COVERAGE_INFO}
+ COMMAND ${EXCLUDE_COMMAND}
+ DEPENDS ccov-preprocessing ccov-all-processing)
+ else()
+ add_custom_target(
+ ccov-all-capture
+ COMMAND ${CMAKE_COMMAND} -E rm -f ${COVERAGE_INFO}
+ COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture
+ --output-file ${COVERAGE_INFO}
+ COMMAND ${EXCLUDE_COMMAND}
+ DEPENDS ccov-preprocessing ccov-all-processing)
+ endif()
+
+ # Generates HTML output of all targets for perusal
+ add_custom_target(
+ ccov-all
+ COMMAND ${GENHTML_PATH} -o ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged
+ ${COVERAGE_INFO} -p ${CMAKE_SOURCE_DIR}
+ DEPENDS ccov-all-capture)
+
+ endif()
+
+ add_custom_command(
+ TARGET ccov-all
+ POST_BUILD
+ COMMAND ;
+ COMMENT
+ "Open ${CMAKE_COVERAGE_OUTPUT_DIRECTORY}/all-merged/index.html in your browser to view the coverage report."
+ )
+ endif()
+endfunction()
--- /dev/null
+# SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+cmake_policy(SET CMP0140 NEW)
+
+function(detectAndroidNDKVersion outVar)
+ if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.20)
+ # CMAKE_ANDROID_NDK_VERSION introduced in CMake 3.20
+ set(${outVar} "${CMAKE_ANDROID_NDK_VERSION}")
+ else()
+ if (NOT CMAKE_ANDROID_NDK)
+ message(STATUS "Couldn't detect Android NDK version: CMAKE_ANDROID_NDK not set")
+ return()
+ endif()
+ if (NOT EXISTS "${CMAKE_ANDROID_NDK}/source.properties")
+ message(STATUS "Couldn't detect Android NDK version: ${CMAKE_ANDROID_NDK}/source.properties doesn't exist")
+ return()
+ endif()
+ file(STRINGS "${CMAKE_ANDROID_NDK}/source.properties" _sources REGEX "^Pkg\.Revision = [0-9]+\.[0-9]+\.[0-9]+$")
+ string(REGEX MATCH "= ([0-9]+\.[0-9]+)\." _match "${_sources}")
+ set(${outVar} "${CMAKE_MATCH_1}")
+ if (NOT ${outVar})
+ message(STATUS "Couldn't detect Android NDK version: ${CMAKE_ANDROID_NDK}/source.properties doesn't contain Pkg.Revision")
+ return()
+ endif()
+
+ endif()
+
+ message(STATUS "Detected Android NDK version ${${outVar}}")
+ return(PROPAGATE ${outVar})
+endfunction()
--- /dev/null
+# SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+#[=======================================================================[.rst:
+ECMGeneratePriFile
+------------------
+
+Generate a ``.pri`` file for the benefit of qmake-based projects.
+
+As well as the function below, this module creates the cache variable
+``ECM_MKSPECS_INSTALL_DIR`` and sets the default value to ``mkspecs/modules``.
+This assumes Qt and the current project are both installed to the same
+non-system prefix. Packagers who use ``-DCMAKE_INSTALL_PREFIX=/usr`` will
+certainly want to set ``ECM_MKSPECS_INSTALL_DIR`` to something like
+``share/qt5/mkspecs/modules``.
+
+The main thing is that this should be the ``modules`` subdirectory of either
+the default qmake ``mkspecs`` directory or of a directory that will be in the
+``$QMAKEPATH`` environment variable when qmake is run.
+
+::
+
+ ecm_generate_pri_file(BASE_NAME <baseName>
+ LIB_NAME <libName>
+ [VERSION <version>] # since 5.83
+ [DEPS "<dep> [<dep> [...]]"]
+ [FILENAME_VAR <filename_variable>]
+ [INCLUDE_INSTALL_DIRS <dir> [<dir> [...]]] # since 5.92
+ [INCLUDE_INSTALL_DIR <dir>] # deprecated since 5.92
+ [LIB_INSTALL_DIR <dir>])
+
+If your CMake project produces a Qt-based library, you may expect there to be
+applications that wish to use it that use a qmake-based build system, rather
+than a CMake-based one. Creating a ``.pri`` file will make use of your
+library convenient for them, in much the same way that CMake config files make
+things convenient for CMake-based applications. ``ecm_generate_pri_file()``
+generates just such a file.
+
+VERSION specifies the version of the library the ``.pri`` file describes. If
+not set, the value is taken from the context variable ``PROJECT_VERSION``.
+This variable is usually set by the ``project(... VERSION ...)`` command or,
+if CMake policy CMP0048 is not NEW, by :module:`ECMSetupVersion`.
+For backward-compatibility with older ECM versions the
+``PROJECT_VERSION_STRING`` variable as set by :module:`ECMSetupVersion`
+will be preferred over ``PROJECT_VERSION`` if set, unless the minimum
+required version of ECM is 5.83 and newer. Since 5.83.
+
+BASE_NAME specifies the name qmake project (.pro) files should use to refer to
+the library (eg: KArchive). LIB_NAME is the name of the actual library to
+link to (ie: the first argument to add_library()). DEPS is a space-separated
+list of the base names of other libraries (for Qt libraries, use the same
+names you use with the ``QT`` variable in a qmake project file, such as "core"
+for QtCore). FILENAME_VAR specifies the name of a variable to store the path
+to the generated file in.
+
+INCLUDE_INSTALL_DIRS are the paths (relative to ``CMAKE_INSTALL_PREFIX``) that
+include files will be installed to. It defaults to
+``${INCLUDE_INSTALL_DIR}/<baseName>`` if the ``INCLUDE_INSTALL_DIR`` variable
+is set. If that variable is not set, the ``CMAKE_INSTALL_INCLUDEDIR`` variable
+is used instead, and if neither are set ``include`` is used. LIB_INSTALL_DIR
+operates similarly for the installation location for libraries; it defaults to
+``${LIB_INSTALL_DIR}``, ``${CMAKE_INSTALL_LIBDIR}`` or ``lib``, in that order.
+
+INCLUDE_INSTALL_DIR is the old variant of INCLUDE_INSTALL_DIRS, taking only one
+directory.
+
+Example usage:
+
+.. code-block:: cmake
+
+ ecm_generate_pri_file(
+ BASE_NAME KArchive
+ LIB_NAME KF5KArchive
+ DEPS "core"
+ FILENAME_VAR pri_filename
+ VERSION 4.2.0
+ )
+ install(FILES ${pri_filename} DESTINATION ${ECM_MKSPECS_INSTALL_DIR})
+
+A qmake-based project that wished to use this would then do::
+
+ QT += KArchive
+
+in their ``.pro`` file.
+
+Since pre-1.0.0.
+#]=======================================================================]
+
+# Replicate the logic from KDEInstallDirs.cmake as we can't depend on it
+# Ask qmake if we're using the same prefix as Qt
+set(_should_query_qt OFF)
+if(NOT DEFINED KDE_INSTALL_USE_QT_SYS_PATHS)
+ include(ECMQueryQt)
+ ecm_query_qt(qt_install_prefix_dir QT_INSTALL_PREFIX TRY)
+ if(qt_install_prefix_dir STREQUAL "${CMAKE_INSTALL_PREFIX}")
+ set(_should_query_qt ON)
+ endif()
+endif()
+
+if(KDE_INSTALL_USE_QT_SYS_PATHS OR _should_query_qt)
+ include(ECMQueryQt)
+ ecm_query_qt(qt_install_prefix_dir QT_INSTALL_PREFIX)
+ ecm_query_qt(qt_host_data_dir QT_HOST_DATA)
+ if(qt_install_prefix_dir STREQUAL "${CMAKE_INSTALL_PREFIX}")
+ file(RELATIVE_PATH qt_host_data_dir ${qt_install_prefix_dir} ${qt_host_data_dir})
+ endif()
+ if(qt_host_data_dir STREQUAL "")
+ set(mkspecs_install_dir mkspecs/modules)
+ else()
+ set(mkspecs_install_dir ${qt_host_data_dir}/mkspecs/modules)
+ endif()
+ set(ECM_MKSPECS_INSTALL_DIR ${mkspecs_install_dir} CACHE PATH "The directory where mkspecs will be installed to.")
+else()
+ set(ECM_MKSPECS_INSTALL_DIR mkspecs/modules CACHE PATH "The directory where mkspecs will be installed to.")
+endif()
+
+function(ECM_GENERATE_PRI_FILE)
+ set(options INTERFACE)
+ set(oneValueArgs BASE_NAME LIB_NAME DEPS FILENAME_VAR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR VERSION)
+ set(multiValueArgs INCLUDE_INSTALL_DIRS)
+
+ cmake_parse_arguments(EGPF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if(EGPF_UNPARSED_ARGUMENTS)
+ message(FATAL_ERROR "Unknown keywords given to ECM_GENERATE_PRI_FILE(): \"${EGPF_UNPARSED_ARGUMENTS}\"")
+ endif()
+
+ if(ECM_GLOBAL_FIND_VERSION VERSION_LESS 5.83.0)
+ set(_support_backward_compat_version_string_var TRUE)
+ else()
+ set(_support_backward_compat_version_string_var FALSE)
+ endif()
+
+ if(NOT EGPF_BASE_NAME)
+ message(FATAL_ERROR "Required argument BASE_NAME missing in ECM_GENERATE_PRI_FILE() call")
+ endif()
+ if(NOT EGPF_LIB_NAME)
+ message(FATAL_ERROR "Required argument LIB_NAME missing in ECM_GENERATE_PRI_FILE() call")
+ endif()
+ if(NOT EGPF_VERSION)
+ if(_support_backward_compat_version_string_var)
+ if(NOT PROJECT_VERSION_STRING AND NOT PROJECT_VERSION)
+ message(FATAL_ERROR "Required variable PROJECT_VERSION_STRING or PROJECT_VERSION not set before ECM_GENERATE_PRI_FILE() call. Missing call of ecm_setup_version() or project(VERSION)?")
+ endif()
+ else()
+ if(NOT PROJECT_VERSION)
+ message(FATAL_ERROR "Required variable PROJECT_VERSION not set before ECM_GENERATE_PRI_FILE() call. Missing call of ecm_setup_version() or project(VERSION)?")
+ endif()
+ endif()
+ endif()
+ if(EGPF_INCLUDE_INSTALL_DIR)
+ if(EGPF_INCLUDE_INSTALL_DIRS)
+ message(FATAL_ERROR "Only one argument of INCLUDE_INSTALL_DIR & INCLUDE_INSTALL_DIRS can be used in ECM_GENERATE_PRI_FILE() call")
+ endif()
+ set(EGPF_INCLUDE_INSTALL_DIRS ${EGPF_INCLUDE_INSTALL_DIR})
+ endif()
+ if(NOT EGPF_INCLUDE_INSTALL_DIRS)
+ if(INCLUDE_INSTALL_DIR)
+ set(EGPF_INCLUDE_INSTALL_DIRS "${INCLUDE_INSTALL_DIR}/${EGPF_BASE_NAME}")
+ elseif(CMAKE_INSTALL_INCLUDEDIR)
+ set(EGPF_INCLUDE_INSTALL_DIRS "${CMAKE_INSTALL_INCLUDEDIR}/${EGPF_BASE_NAME}")
+ else()
+ set(EGPF_INCLUDE_INSTALL_DIRS "include/${EGPF_BASE_NAME}")
+ endif()
+ endif()
+ if(NOT EGPF_LIB_INSTALL_DIR)
+ if(LIB_INSTALL_DIR)
+ set(EGPF_LIB_INSTALL_DIR "${LIB_INSTALL_DIR}")
+ elseif(CMAKE_INSTALL_LIBDIR)
+ set(EGPF_LIB_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}")
+ else()
+ set(EGPF_LIB_INSTALL_DIR "lib")
+ endif()
+ endif()
+
+ if(EGPF_VERSION)
+ set(PRI_VERSION "${EGPF_VERSION}")
+ else()
+ if(_support_backward_compat_version_string_var AND PROJECT_VERSION_STRING)
+ set(PRI_VERSION "${PROJECT_VERSION_STRING}")
+ if(NOT PROJECT_VERSION_STRING STREQUAL PROJECT_VERSION)
+ message(DEPRECATION "ECM_GENERATE_PRI_FILE() will no longer support PROJECT_VERSION_STRING when the required minimum version of ECM is 5.83 or newer. Set VERSION parameter or use PROJECT_VERSION instead.")
+ endif()
+ else()
+ set(PRI_VERSION "${PROJECT_VERSION}")
+ endif()
+ endif()
+
+ string(REGEX REPLACE "^([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" PRI_VERSION_MAJOR "${PRI_VERSION}")
+ string(REGEX REPLACE "^[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" PRI_VERSION_MINOR "${PRI_VERSION}")
+ string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" PRI_VERSION_PATCH "${PRI_VERSION}")
+
+ # Prepare the right number of "../.." to go from ECM_MKSPECS_INSTALL_DIR to the install prefix
+ # This allows to make the generated pri files relocatable (no absolute paths)
+ if (IS_ABSOLUTE ${ECM_MKSPECS_INSTALL_DIR})
+ set(BASEPATH ${CMAKE_INSTALL_PREFIX})
+ else()
+ string(REGEX REPLACE "[^/]+" ".." PRI_ROOT_RELATIVE_TO_MKSPECS ${ECM_MKSPECS_INSTALL_DIR})
+ set(BASEPATH "$$PWD/${PRI_ROOT_RELATIVE_TO_MKSPECS}")
+ endif()
+
+ set(PRI_TARGET_BASENAME ${EGPF_BASE_NAME})
+ set(PRI_TARGET_LIBNAME ${EGPF_LIB_NAME})
+ set(PRI_TARGET_QTDEPS ${EGPF_DEPS})
+ set(PRI_TARGET_INCLUDES)
+ foreach(_dir ${EGPF_INCLUDE_INSTALL_DIRS})
+ # separate list entries with space
+ if(IS_ABSOLUTE "${_dir}")
+ string(APPEND PRI_TARGET_INCLUDES " ${_dir}")
+ else()
+ string(APPEND PRI_TARGET_INCLUDES " ${BASEPATH}/${_dir}")
+ endif()
+ endforeach()
+ if(IS_ABSOLUTE "${EGPF_LIB_INSTALL_DIR}")
+ set(PRI_TARGET_LIBS "${EGPF_LIB_INSTALL_DIR}")
+ else()
+ set(PRI_TARGET_LIBS "${BASEPATH}/${EGPF_LIB_INSTALL_DIR}")
+ endif()
+ set(PRI_TARGET_DEFINES "")
+
+ set(PRI_FILENAME ${CMAKE_CURRENT_BINARY_DIR}/qt_${PRI_TARGET_BASENAME}.pri)
+ if (EGPF_FILENAME_VAR)
+ set(${EGPF_FILENAME_VAR} ${PRI_FILENAME} PARENT_SCOPE)
+ endif()
+
+ set(PRI_TARGET_MODULE_CONFIG "")
+ # backward compat: it was not obvious LIB_NAME needs to be a target name,
+ # and some projects where the target name was not the actual library output name
+ # passed the output name for LIB_NAME, so .name & .module prperties are correctly set.
+ # TODO: improve API dox, allow control over module name if target name != output name
+ if(TARGET ${EGPF_LIB_NAME})
+ get_target_property(target_type ${EGPF_LIB_NAME} TYPE)
+ if (target_type STREQUAL "STATIC_LIBRARY")
+ set(PRI_TARGET_MODULE_CONFIG "staticlib")
+ endif()
+ endif()
+
+ if (EGPF_INTERFACE)
+ set(PRI_TARGET_MODULE "")
+ else()
+ set(PRI_TARGET_MODULE "${PRI_TARGET_LIBNAME}")
+ endif()
+
+
+ file(GENERATE
+ OUTPUT ${PRI_FILENAME}
+ CONTENT
+ "QT.${PRI_TARGET_BASENAME}.VERSION = ${PRI_VERSION}
+QT.${PRI_TARGET_BASENAME}.MAJOR_VERSION = ${PRI_VERSION_MAJOR}
+QT.${PRI_TARGET_BASENAME}.MINOR_VERSION = ${PRI_VERSION_MINOR}
+QT.${PRI_TARGET_BASENAME}.PATCH_VERSION = ${PRI_VERSION_PATCH}
+QT.${PRI_TARGET_BASENAME}.name = ${PRI_TARGET_LIBNAME}
+QT.${PRI_TARGET_BASENAME}.module = ${PRI_TARGET_MODULE}
+QT.${PRI_TARGET_BASENAME}.defines = ${PRI_TARGET_DEFINES}
+QT.${PRI_TARGET_BASENAME}.includes = ${PRI_TARGET_INCLUDES}
+QT.${PRI_TARGET_BASENAME}.private_includes =
+QT.${PRI_TARGET_BASENAME}.libs = ${PRI_TARGET_LIBS}
+QT.${PRI_TARGET_BASENAME}.depends = ${PRI_TARGET_QTDEPS}
+QT.${PRI_TARGET_BASENAME}.module_config = ${PRI_TARGET_MODULE_CONFIG}
+"
+ )
+endfunction()
--- /dev/null
+# SPDX-FileCopyrightText: 2014 Rohan Garg <rohan16garg@gmail.com>
+# SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
+# SPDX-FileCopyrightText: 2014-2016 Aleix Pol <aleixpol@kde.org>
+# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau <kossebau@kde.org>
+# SPDX-FileCopyrightText: 2022 Ahmad Samir <a.samir78@gmail.com>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#[=======================================================================[.rst:
+ECMQueryQt
+---------------
+This module can be used to query the installation paths used by Qt.
+
+For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in
+support to query the paths of a target platform when cross-compiling).
+
+This module defines the following function:
+::
+
+ ecm_query_qt(<result_variable> <qt_variable> [TRY])
+
+Passing ``TRY`` will result in the method not making the build fail if the executable
+used for querying has not been found, but instead simply print a warning message and
+return an empty string.
+
+Example usage:
+
+.. code-block:: cmake
+
+ include(ECMQueryQt)
+ ecm_query_qt(bin_dir QT_INSTALL_BINS)
+
+If the call succeeds ``${bin_dir}`` will be set to ``<prefix>/path/to/bin/dir`` (e.g.
+``/usr/lib64/qt/bin/``).
+
+Since: 5.93
+#]=======================================================================]
+
+if (QT_VERSION_MAJOR STREQUAL "5")
+ find_package(Qt${QT_VERSION_MAJOR}Core QUIET)
+ get_target_property(_qmake_executable_default Qt5::qmake LOCATION)
+ set(QUERY_EXECUTABLE ${_qmake_executable_default}
+ CACHE FILEPATH "Location of the Qt5 qmake executable")
+ set(_exec_name_text "Qt5 qmake")
+ set(_cli_option "-query")
+elseif(QT_VERSION_MAJOR STREQUAL "6")
+ find_package(Qt6 COMPONENTS CoreTools REQUIRED CONFIG)
+ get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION)
+ set(QUERY_EXECUTABLE ${_qtpaths_executable}
+ CACHE FILEPATH "Location of the Qt6 qtpaths executable")
+ set(_exec_name_text "Qt6 qtpaths")
+ set(_cli_option "--query")
+endif()
+
+function(ecm_query_qt result_variable qt_variable)
+ set(options TRY)
+ set(oneValueArgs)
+ set(multiValueArgs)
+
+ cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if(NOT QUERY_EXECUTABLE)
+ if(ARGS_TRY)
+ set(${result_variable} "" PARENT_SCOPE)
+ message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}")
+ return()
+ else()
+ message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required")
+ endif()
+ endif()
+ execute_process(
+ COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}"
+ RESULT_VARIABLE return_code
+ OUTPUT_VARIABLE output
+ )
+ if(return_code EQUAL 0)
+ string(STRIP "${output}" output)
+ file(TO_CMAKE_PATH "${output}" output_path)
+ set(${result_variable} "${output_path}" PARENT_SCOPE)
+ else()
+ message(WARNING "Failed call: ${_command} \"${qt_variable}\"")
+ message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}")
+ endif()
+endfunction()
--- /dev/null
+function(generate_headers output_var)
+ set(options)
+ set(oneValueArgs OUTPUT_DIR ORIGINAL_PREFIX ORIGINAL_HEADERS_VAR)
+ set(multiValueArgs HEADER_NAMES)
+ cmake_parse_arguments(GH "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if (GH_OUTPUT_DIR)
+ set(GH_OUTPUT_DIR "${GH_OUTPUT_DIR}/")
+ endif()
+
+ foreach(_headername ${GH_HEADER_NAMES})
+ string(TOLOWER "${_headername}" originalbase)
+ set(CC_ORIGINAL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${GH_ORIGINAL_PREFIX}/${originalbase}.h")
+ if (NOT EXISTS ${CC_ORIGINAL_FILE})
+ message(FATAL_ERROR "Could not find header \"${CC_ORIGINAL_FILE}\"")
+ endif()
+
+ set(CC_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/${GH_OUTPUT_DIR}/${_headername}")
+ if (NOT EXISTS ${CC_HEADER_FILE})
+ file(WRITE ${CC_HEADER_FILE} "#include \"${GH_ORIGINAL_PREFIX}${originalbase}.h\"")
+ endif()
+ list(APPEND ${output_var} ${CC_HEADER_FILE})
+ list(APPEND ${GH_ORIGINAL_HEADERS_VAR} ${CC_ORIGINAL_FILE})
+ endforeach()
+
+ set(${output_var} ${${output_var}} PARENT_SCOPE)
+ set(${GH_ORIGINAL_HEADERS_VAR} ${${GH_ORIGINAL_HEADERS_VAR}} PARENT_SCOPE)
+endfunction()
--- /dev/null
+include(CMakePackageConfigHelpers)
+
+function(generate_cmake_module_config_file)
+ function(process_dependencies)
+ set(oneValueArgs PREFIX OUTPUT)
+ set(multiValueArgs LIBRARIES)
+ cmake_parse_arguments(deps "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ set(_deps)
+ set(_deps_private FALSE)
+ foreach(dep ${deps_LIBRARIES})
+ if ("${dep}" MATCHES "PUBLIC|INTERFACE")
+ set(_deps_private FALSE)
+ continue()
+ elseif ("${dep}" STREQUAL "PRIVATE")
+ set(_deps_private TRUE)
+ continue()
+ endif()
+
+ if (NOT _deps_private)
+ set(_deps "${_deps}find_dependency(${deps_PREFIX}${dep})\n")
+ endif()
+ endforeach()
+
+ set(${deps_OUTPUT} ${_deps} PARENT_SCOPE)
+ endfunction()
+
+ set(options)
+ set(oneValueArgs TARGET_NAME NAME)
+ set(multiValueArgs QT_DEPENDENCIES QCORO_DEPENDENCIES)
+ cmake_parse_arguments(cmc "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ process_dependencies(
+ PREFIX "Qt${QT_VERSION_MAJOR}"
+ LIBRARIES ${cmc_QT_DEPENDENCIES}
+ OUTPUT QT_DEPENDENCIES
+ )
+
+ process_dependencies(
+ PREFIX "QCoro${QT_VERSION_MAJOR}"
+ LIBRARIES ${cmc_QCORO_DEPENDENCIES}
+ OUTPUT QCORO_DEPENDENCIES
+ )
+
+ set(MODULE_NAME "${cmc_NAME}")
+ configure_package_config_file(
+ "${qcoro_SOURCE_DIR}/cmake/QCoroModuleConfig.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/${cmc_TARGET_NAME}Config.cmake"
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${cmc_TARGET_NAME}
+ PATH_VARS CMAKE_INSTALL_INCLUDEDIR
+ )
+
+ write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/${cmc_TARGET_NAME}ConfigVersion.cmake"
+ VERSION ${qcoro_VERSION}
+ COMPATIBILITY SameMajorVersion
+ )
+endfunction()
--- /dev/null
+macro(qcoro_find_qt)
+ set(options)
+ set(oneValueArgs QT_VERSION FOUND_VER_VAR)
+ set(multiValueArgs COMPONENTS QT5_COMPONENTS QT6_COMPONENTS)
+ cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if (NOT ARGS_QT_VERSION)
+ find_package(Qt6Core QUIET)
+ if (Qt6Core_FOUND)
+ set(ARGS_QT_VERSION 6)
+ else()
+ set(ARGS_QT_VERSION 5)
+ endif()
+ endif()
+
+ list(APPEND REQUIRED_QT_COMPONENTS "${ARGS_QT${ARGS_QT_VERSION}_COMPONENTS}")
+ list(FILTER REQUIRED_QT_COMPONENTS EXCLUDE REGEX "Private$$")
+
+ find_package(Qt${ARGS_QT_VERSION} REQUIRED COMPONENTS ${REQUIRED_QT_COMPONENTS})
+
+ if ("${ARGS_QT_VERSION}" STREQUAL "6" AND Qt6_VERSION VERSION_GREATER_EQUAL "6.10.0")
+ list(APPEND REQUIRED_PRIVATE_QT_COMPONENTS "${ARGS_COMPONENTS}")
+ list(APPEND REQUIRED_PRIVATE_QT_COMPONENTS "${ARGS_QT${ARGS_QT_VERSION}_COMPONENTS}")
+ list(FILTER REQUIRED_PRIVATE_QT_COMPONENTS INCLUDE REGEX "Private$$")
+
+ if (REQUIRED_PRIVATE_QT_COMPONENTS)
+ find_package(Qt${ARGS_QT_VERSION} REQUIRED COMPONENTS ${REQUIRED_PRIVATE_QT_COMPONENTS})
+ endif()
+ endif()
+
+ set(${ARGS_FOUND_VER_VAR} ${ARGS_QT_VERSION})
+endmacro()
--- /dev/null
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+@QT_DEPENDENCIES@
+@QCORO_DEPENDENCIES@
+
+include("${CMAKE_CURRENT_LIST_DIR}/QCoro@QT_VERSION_MAJOR@@MODULE_NAME@Targets.cmake")
+
+# Versionless target, for compatiblity with Qt6
+if (TARGET QCoro@QT_VERSION_MAJOR@::@MODULE_NAME@ AND NOT TARGET QCoro::@MODULE_NAME@)
+ add_library(QCoro::@MODULE_NAME@ INTERFACE IMPORTED)
+ set_target_properties(QCoro::@MODULE_NAME@ PROPERTIES
+ INTERFACE_LINK_LIBRARIES "QCoro@QT_VERSION_MAJOR@::@MODULE_NAME@"
+ )
+endif()
--- /dev/null
+#cmakedefine QCORO_QT_HAS_COMPAT_ABI
--- /dev/null
+# SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+# SPDX-License-Identifier: MIT
+#
+# Docker image for specific versions of clang and specific version of Qt.
+
+FROM debian:bullseye
+ARG compiler_version
+ARG qt_version
+ARG qt_modules
+ARG qt_archives
+
+SHELL ["/bin/bash", "-c"]
+
+# Install build & runtime dependencies
+RUN apt-get update \
+ && apt-get upgrade --yes \
+ && apt-get install --yes --no-install-recommends \
+ cmake=3.18.4\* cmake-data=3.18.4\* \
+ make \
+ python3-pip python3-setuptools python3-wheel python3-dev \
+ dbus dbus-x11 \
+ libglib2.0-dev libxkbcommon-dev libfreetype6-dev libfontconfig1-dev \
+ libssl-dev \
+ libegl-dev libgl-dev libegl1=1.3.2\* libgl1=1.3.2\* libglx0=1.3.2\* libglvnd0=1.3.2\* \
+ libvulkan-dev
+
+# Install and set up clang
+RUN \
+ # Set up env \
+ if [ "${compiler_version}" == "dev" ]; then \
+ export CLANG_VERSION_SUFFIX=""; \
+ else \
+ export CLANG_VERSION_SUFFIX="-${compiler_version}"; \
+ fi && \
+ echo "export CC=\"/usr/bin/clang${CLANG_VERSION_SUFFIX}\"" >> /etc/profile.d/clang.sh && \
+ echo "export CXX=\"/usr/bin/clang++${CLANG_VERSION_SUFFIX}\"" >> /etc/profile.d/clang.sh && \
+ # Install tools needed to add the clang repository \
+ apt-get install --yes --no-install-recommends ca-certificates wget gnupg && \
+ # Add clang repository \
+ echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye${CLANG_VERSION_SUFFIX} main" > /etc/apt/sources.list.d/llvm.list && \
+ echo "deb-src http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye${CLANG_VERSION_SUFFIX} main" >> /etc/apt/sources.list.d/llvm.list && \
+ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \
+ # Install clang \
+ apt-get update && \
+ apt-get install --yes --install-recommends clang${CLANG_VERSION_SUFFIX} && \
+ clang_major_version=$(clang${CLANG_VERSION_SUFFIX} --version | grep -Eo "[0-9]+.[0-9]+.[0-9]+" | cut -d'.' -f1) && \
+ if [ ${clang_major_version} -gt 11 ]; then \
+ apt-get install --yes --no-install-recommends libclang-rt-${clang_major_version}-dev; \
+ fi
+
+# Workaround a bug in CMake (?) which tries to look for OpenGL-related libraries
+# in /usr/lib/x86_64-unknown-linux-gnu instead of /usr/lib/x86_64-linux-gnu
+RUN ln -s x86_64-linux-gnu /usr/lib/x86_64-unknown-linux-gnu
+
+# Install Qt
+WORKDIR /root
+RUN pip3 install 'aqtinstall==3.2.0'
+COPY install-qt.sh ./install-qt.sh
+RUN ./install-qt.sh "${qt_version}" "${qt_modules}" "${qt_archives}"
+
+# Set Qt up environment
+ENV QT_BASE_DIR "/opt/qt/${qt_version}/gcc_64"
+ENV PATH "${QT_BASE_DIR}/bin:${PATH}"
+ENV CMAKE_PREFIX_PATH "${QT_BASE_DIR}/lib/cmake"
+ENV LD_LIBRARY_PATH "${QT_BASE_DIR}/lib:${LD_LIBRARY_PATH}"
+ENV XDG_DATA_DIRS "${QT_BASE_DIR}/share:${XDG_DATA_DIRS}"
+
+ENTRYPOINT [ "/bin/bash", "-l" ]
--- /dev/null
+# SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+# SPDX-License-Identifier: MIT
+#
+# Docker image for specific versions of gcc and specific version of Qt.
+
+ARG compiler_version
+FROM gcc:${compiler_version}
+ARG qt_version
+ARG qt_modules
+ARG qt_archives
+
+SHELL ["/bin/bash", "-c"]
+
+# Enable backports in older GCC images to get access to newer cmake
+RUN source /etc/os-release && \
+ if [[ "${VERSION_ID}" == "10" ]]; then \
+ echo "deb http://deb.debian.org/debian buster-backports main" > /etc/apt/sources.list.d/backports.list; \
+ fi
+
+# Install build-time and runtime dependencies
+RUN apt-get update \
+ && apt-get upgrade --yes \
+ && apt-get install --yes --no-install-recommends \
+ cmake cmake-data \
+ python3-pip python3-setuptools python3-wheel python3-dev \
+ dbus dbus-x11 \
+ libglib2.0-dev libxkbcommon-dev libfreetype6-dev libfontconfig1-dev \
+ libssl-dev \
+ libegl-dev libgl-dev libegl1 libgl1 libglx0 libglvnd0 \
+ libvulkan-dev \
+ && apt-get clean
+
+# Workaround a bug in CMake (?) which tries to look for OpenGL-related libraries
+# in /usr/lib/x86_64-unknown-linux-gnu instead of /usr/lib/x86_64-linux-gnu
+RUN ln -s x86_64-linux-gnu /usr/lib/x86_64-unknown-linux-gnu
+
+# Install Qt
+WORKDIR /root
+# Necessary to allow using pip on newer images (--break-system-packages doesn't work on older images)
+RUN rm -f /usr/lib/python$(python3 --version | grep -oE "3.[0-9]+")/EXTERNALLY-MANAGED \
+ && pip3 install 'aqtinstall==3.2.0'
+COPY install-qt.sh ./install-qt.sh
+RUN ./install-qt.sh "${qt_version}" "${qt_modules}" "${qt_archives}"
+
+# Set up environment
+ENV QT_BASE_DIR "/opt/qt/${qt_version}/gcc_64"
+ENV PATH "${QT_BASE_DIR}/bin:${PATH}"
+ENV CMAKE_PREFIX_PATH="${QT_BASE_DIR}/lib/cmake"
+ENV LD_LIBRARY_PATH "${QT_BASE_DIR}/lib:${LD_LIBRARY_PATH}"
+ENV XDG_DATA_DIRS "${QT_BASE_DIR}/share:${XDG_DATA_DIRS}"
--- /dev/null
+#!/bin/bash
+
+set -e
+
+qt_version="$1"
+qt_modules="$2"
+qt_archives="$3"
+
+if [[ -n "${qt_archives}" ]]; then
+ opt_archives="--archives ${qt_archives}"
+fi
+if [[ -n "${qt_modules}" ]]; then
+ opt_modules="--modules ${qt_modules}"
+fi
+
+echo "Installing Qt ${qt_version}"
+echo "Modules: ${qt_modules}"
+echo "Archives: ${qt_archives}"
+aqt install-qt -O /opt/qt linux desktop "${qt_version}" gcc_64 ${opt_archives} ${opt_modules}
+echo "Done."
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# License
+
+---
+
+QCoro is published under the MIT License
+
+## MIT License
+
+Copyright (c) 2021 Daniel Vrátil <dvratil@kde.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" height="640" viewBox="0 0 169.333 169.333"><defs><linearGradient xlink:href="#a" id="e" x1="67.139" x2="113.404" y1="169.332" y2="214.084" gradientTransform="translate(18.88 -9.719)" gradientUnits="userSpaceOnUse"/><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#c7c9c8;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#b" id="f" x1="72.921" x2="158.516" y1="127.799" y2="158.312" gradientTransform="translate(18.88 -9.719)" gradientUnits="userSpaceOnUse"/><linearGradient id="b"><stop offset="0" style="stop-color:#00ca87;stop-opacity:1"/><stop offset="1" style="stop-color:#259a78;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#c" id="g" x1="254.729" x2="263.104" y1="118.44" y2="111.145" gradientTransform="translate(-129.954 81.409)" gradientUnits="userSpaceOnUse"/><linearGradient id="c"><stop offset=".215" style="stop-color:#000;stop-opacity:0"/><stop offset="1" style="stop-color:#000;stop-opacity:.72590619"/></linearGradient><linearGradient xlink:href="#d" id="i" x1="78.129" x2="77.767" y1="149.188" y2="81.556" gradientUnits="userSpaceOnUse"/><linearGradient id="d"><stop offset="0" style="stop-color:#4ddaa3;stop-opacity:1"/><stop offset=".132" style="stop-color:#4ddaa3;stop-opacity:1"/><stop offset=".525" style="stop-color:#fff;stop-opacity:1"/></linearGradient><filter id="h" width="1.177" height="1.23" x="-.088" y="-.115" style="color-interpolation-filters:sRGB"><feGaussianBlur stdDeviation="1.442"/></filter></defs><path d="m75.732 104.503 10.172 10.69M62.327 128.936l11.108 4.676" style="fill:none;stroke:#1a1a1a;stroke-width:2.64583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M56.835 156.72h10.082" style="fill:none;stroke:#1a1a1a;stroke-width:2.11667;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="m61.435 179.774 9.083-3.8M75.412 203.306l8.205-8.555M101.159 216.681l3.535-9.215" style="fill:none;stroke:#1a1a1a;stroke-width:3.96875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="m182.81 180.001-8.835-5.16" style="fill:none;stroke:#1a1a1a;stroke-width:2.64583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M186.504 155.506h-8.223" style="fill:none;stroke:#1a1a1a;stroke-width:2.11667;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="m183.897 134.193-9.41 4.165M174.348 114.217l-8.47 6.06M160.265 100.323l-6.8 9.102" style="fill:none;stroke:#1a1a1a;stroke-width:2.64583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M122.196 97.702a57.944 57.944 0 0 0-57.944 57.945 57.944 57.944 0 0 0 57.944 57.943 57.944 57.944 0 0 0 57.943-57.943 57.944 57.944 0 0 0-57.943-57.945m0 28.67a29.274 29.274 0 0 1 28.705 23.536 29.34 27.792 0 0 1 .635 5.739 29.34 27.792 0 0 1-.213 2.05 29 29 0 0 1-.283 2.721 29.34 27.792 0 0 1-.298 1.369 29.274 29.274 0 0 1-28.546 23.133 29.274 29.274 0 0 1-28.547-23.133 29.34 27.792 0 0 1-.298-1.369 29 29 0 0 1-.283-2.721 29.34 27.792 0 0 1-.213-2.05 29.34 27.792 0 0 1 .213-2.049 29 29 0 0 1 .283-2.725 29.34 27.792 0 0 1 .297-1.366 29.274 29.274 0 0 1 28.548-23.135" style="fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:7.74907;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M174.039 155.166h-22.612l.042.48a29.274 29.274 0 0 1-29.273 29.274 29 29 0 0 1-3.502-.25 27.79 29.338 18.333 0 1-.57-.041 29 29 0 0 1-4.327-.951 27.79 29.338 18.333 0 1-.83-.182 27.79 29.338 18.333 0 1-1.632-.736 29 29 0 0 1-2.365-1.067 27.79 29.338 18.333 0 1-1.477-.807 29.27 29.27 0 0 1-14.571-25.24l.047-.48H70.416a51.86 51.86 0 0 0 35.467 49.706 51.86 51.86 0 0 0 65.539-32.913 51.9 51.9 0 0 0 2.617-16.793" style="fill:url(#e);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M123.979 103.818a51.86 51.86 0 0 0-51.01 35.515 51.9 51.9 0 0 0-2.553 15.833h22.553a29.274 29.274 0 0 1 29.227-28.794 29 29 0 0 1 3.777.254 27.79 29.338 18.333 0 1 .277.034 29 29 0 0 1 4.475.984 27.79 29.338 18.333 0 1 .7.153 27.79 29.338 18.333 0 1 5.239 2.41 29.27 29.27 0 0 1 14.763 24.959h22.612a51.86 51.86 0 0 0-35.53-48.746 51.9 51.9 0 0 0-14.53-2.602" style="fill:url(#f);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M122.196 119.663c-19.874 0-35.984 16.11-35.984 35.984 0 19.872 16.11 35.982 35.984 35.982 19.872 0 35.983-16.11 35.983-35.982 0-19.874-16.11-35.984-35.983-35.984m0 6.71a29.274 29.274 0 0 1 29.273 29.273 29.274 29.274 0 0 1-29.273 29.274 29.274 29.274 0 0 1-29.274-29.274 29.274 29.274 0 0 1 29.274-29.274" style="fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M62.625 126.262a5.124 5.124 0 0 1-5.124 5.123 5.124 5.124 0 0 1-5.124-5.123 5.124 5.124 0 0 1 5.124-5.124 5.124 5.124 0 0 1 5.124 5.124z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:4.49792;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M78.048 101.027a5.124 5.124 0 0 1-5.124 5.124 5.124 5.124 0 0 1-5.124-5.124 5.124 5.124 0 0 1 5.124-5.124 5.124 5.124 0 0 1 5.124 5.124z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:3.175;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M166.258 96.8a3.93 3.93 0 0 1-3.932 3.933 3.93 3.93 0 0 1-3.932-3.932 3.93 3.93 0 0 1 3.932-3.932 3.93 3.93 0 0 1 3.932 3.932zM183.317 110.809a5.124 5.124 0 0 1-5.124 5.123 5.124 5.124 0 0 1-5.123-5.123 5.124 5.124 0 0 1 5.124-5.124 5.124 5.124 0 0 1 5.123 5.124z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:2.64583;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M56.97 156.452a3.48 3.48 0 0 1-3.482 3.482 3.48 3.48 0 0 1-3.482-3.482 3.48 3.48 0 0 1 3.482-3.481 3.48 3.48 0 0 1 3.481 3.481z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:2.11667;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M193.99 155.17a3.48 3.48 0 0 1-3.482 3.482 3.48 3.48 0 0 1-3.482-3.481 3.48 3.48 0 0 1 3.482-3.482 3.48 3.48 0 0 1 3.481 3.482z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:3.175;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M61.628 181.19a5.124 5.124 0 0 1-5.124 5.124 5.124 5.124 0 0 1-5.124-5.124 5.124 5.124 0 0 1 5.124-5.123 5.124 5.124 0 0 1 5.124 5.123z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:3.96875;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M192.74 181.247a5.124 5.124 0 0 1-5.124 5.124 5.124 5.124 0 0 1-5.124-5.124 5.124 5.124 0 0 1 5.124-5.124 5.124 5.124 0 0 1 5.124 5.124z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:3.175;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M105.509 222.822a6.93 6.93 0 0 1-6.928 6.928 6.93 6.93 0 0 1-6.928-6.928 6.93 6.93 0 0 1 6.928-6.928 6.93 6.93 0 0 1 6.928 6.928z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:4.25536876;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M77.354 207.683a8.407 8.407 0 0 1-8.406 8.406 8.407 8.407 0 0 1-8.407-8.406 8.407 8.407 0 0 1 8.407-8.407 8.407 8.407 0 0 1 8.406 8.407z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:3.175;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M68.907 154.233h20.226v3.5H68.907zM156.799 154.233h20.226v3.5h-20.226z" style="fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M193.278 132.01a5.124 5.124 0 0 1-5.124 5.124 5.124 5.124 0 0 1-5.124-5.124 5.124 5.124 0 0 1 5.124-5.124 5.124 5.124 0 0 1 5.124 5.124z" style="fill:#4ddaa3;fill-opacity:1;stroke:#1a1a1a;stroke-width:4.49792;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M132.247 182.897a29.3 29.3 0 0 1-13.923 3.718 29 29 0 0 1-2.698-.27l-2.933 2.666 21.78 23.964a58 58 0 0 0 17.363-8.525z" style="fill:url(#g);fill-opacity:1;stroke:none;stroke-width:7.74907;stroke-dasharray:none;stroke-opacity:1;filter:url(#h)" transform="matrix(1.11394 0 0 1.12443 -51.526 -97.459)"/><path d="M68.562 80.114h1.613a17.23 17.23 45 0 1 17.227 17.228v44.69a9.125 9.125 135 0 1-9.124 9.124 9.72 9.72 45.026 0 1-9.716-9.725z" style="fill:url(#i);fill-opacity:1;stroke:#1a1a1a;stroke-width:5.70935;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" transform="matrix(.89543 -.87163 .8865 .88041 -60.368 75.598)"/></svg>
\ No newline at end of file
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Building and Using QCoro
+
+## Building QCoro
+
+QCoro uses CMake build system. You can pass following options to the `cmake` command when building
+QCoro to customize the build:
+
+* `-DQCORO_BUILD_EXAMPLES` - whether to build examples or not (`ON` by default).
+* `-DQCORO_BUILD_TESTING` - whether to build tests or not (defaults to `${BUILD_TESTING}`), can be used to disable building QCoro tests when building QCoro as part of a bigger project which has `BUILD_TESTING` enabled.
+* `-DQCORO_ENABLE_ASAN` - whether to build QCoro with AddressSanitizer (`OFF` by default).
+* `-DBUILD_SHARED_LIBS` - whether to build QCoro as a shared library (`OFF` by default).
+* `-DUSE_QT_VERSION` - set to `5` or `6` to force a particular version of Qt. When not set the highest available version is used.
+* `-DQCORO_WITH_QTDBUS` - whether to compile support for QtDBus (`ON` by default).
+* `-DQCORO_WITH_QTNETWORK` - whether to compile support for QtNetwork (`ON` by default).
+* `-DQCORO_WITH_QTWEBSOCKETS` - whether to compile support for QtWebSockets (`ON` by default).
+* `-DQCORO_DISABLE_DEPRECATED_TASK_H` - will not build and install the deprecated task.h header (`OFF` by default).
+
+```
+mkdir build
+cd build
+cmake .. <CMAKE FLAGS>
+make
+# This will install QCoro into /usr/local/ prefix, change it by passing -DCMAKE_INSTALL_PREFIX=/usr
+# to the cmake command above.
+sudo make install
+```
+
+## CMake
+
+Depending on whether you want to use Qt5 or Qt6 build of QCoro, you should use `QCoro5` or QCoro6` in your
+CMake code, respectively. The example below is assuming Qt6:
+
+```cmake
+# Use QCoro5 if you are building for Qt5!
+find_package(QCoro6 REQUIRED COMPONENTS Core Network DBus)
+
+# Set necessary compiler flags to enable coroutine support
+qcoro_enable_coroutines()
+
+...
+
+target_link_libraries(your-target QCoro::Core QCoro::Network QCoro::DBus)
+```
+
+Note the missing Qt version number in the `QCoro` target namespace: QCoro provides both
+versioned (`QCoro5` and `QCoro6`) namespaces as well as version-less namespace, which is
+especially useful for transitioning codebase from Qt5 to Qt6.
+
+## QMake
+
+Using QCoro with QMake projects (`.pro`) is simple: just add the required QCoro modules to the `QT`
+variable:
+
+```
+QT += QCoroCore QCoroNetwork QCoroDBus
+# Enable C++20
+CONFIG += c+=20
+# Enable coroutine support in the compiler
+QMAKE_CXXFLAGS += -fcoroutines
+```
+
+You don't need to worry about Qt5 vs Qt6, qmake will pick up the correct build of QCoro depending
+on whether you are using QMake for Qt5 or Qt6.
+
+Currently it's necessary to manually enable C++20 and coroutine support (unless that's already
+default in your system/compiler settings).
--- /dev/null
+---
+title: Changelog
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Changelog
+
+## 0.11.0 (2024-10-04)
+
+* [Release announcement](news/2024/2024-10-04-qcoro-0.11.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.11.0)
+
+## 0.10.0 (2023-12-05)
+
+* [Release announcement](news/2023/2023-12-05-qcoro-0.10.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.10.0)
+
+## 0.9.0 (2023-04-27)
+
+* [Release announcement](news/2023/2023-04-27-qcoro-0.9.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.9.0)
+
+## 0.8.0 (2023-01-31)
+
+* [Release announcement](news/2023/2023-01-31-qcoro-0.8.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.8.0)
+
+## 0.7.0 (2022-11-20)
+
+* [Release announcement](news/2022/2022-11-17-qcoro-0.7.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.7.0)
+
+## 0.6.0 (2022-07-09)
+
+* [Release announcement](news/2022/2022-07-09-qcoro-0.6.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.6.0)
+
+## 0.5.1 (2022-04-27)
+
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.5.1)
+
+## 0.5.0 (2022-04-25)
+
+* [Release announcement](news/2022/2022-04-25-qcoro-0.5.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.5.0)
+
+## 0.4.0 (2022-01-06)
+
+* [Release announcement](news/2022/2022-01-06-qcoro-0.4.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.4.0)
+
+## 0.3.0 (2021-10-11)
+
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.3.0)
+
+## 0.2.0 (2021-09-08)
+
+* [Release announcement](news/2021/2021-09-08-qcoro-0.2.0-announcement.md)
+* [Github release changelog](https://github.com/danvratil/qcoro/releases/tag/v0.2.0)
+
+## 0.1.0 (2021-08-15)
+
+* Initial release QCoro
+* [Release announcement](news/2021/2021-08-16-qcoro-0.1.0-announcement.md)
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# `co_await` Explained
+
+The following paragraphs try to explain what is a coroutine and what `co_await` does in some
+simple way. I don't guarantee that any of this is factically correct. For more gritty (and
+correct) details, refer to the articles linked at the bottom of this document.
+
+Coroutines, simply put, are like normal functions except that they can be suspended (and resumed)
+in the middle. When a coroutine is suspended, execution returns to the function that has called
+the coroutine. If that function is also a coroutine and is waiting (`co_await`ing) for the current
+coroutine to finish, then it is suspended as well and the execution returns to the function that has
+called that coroutine and so on, until a function that is an actual function (not a coroutine) is reached.
+In case of a regular Qt program, this "top-level" non-coroutine function will be the Qt's event loop -
+which means that while your coroutine, when called from the Qt event loop is suspended, the Qt event
+loop will continue to run until the coroutine is resumed again.
+
+Amongst many other things, this allows you to write asynchronous code as if it were synchronous without
+blocking the Qt event loop and making your application unresponsive. See the different examples in this
+document.
+
+Now let's look at the `co_await` keyword. This keyword tells the compiler that this is the point where
+the coroutine wants to be suspended, until the *awaited* object (the *awaitable*) is ready. Anything type
+can be *awaitable* - either because it directly implements the interface needed by the C++ coroutine
+machinery, or because some external tools (like this library) are provided to wrap that type into something
+that implements the *awaitable* interface.
+
+The C++ coroutines introduce two additional keywords -`co_return` and `co_yield`:
+
+From an application programmer point of view, `co_return` behaves exactly the same as `return`, except that
+you cannot use the regular `return` in coroutines. There are some major differences under the hood, though,
+which is likely why there's a special keyword for returning from coroutines.
+
+`co_yield` allows a coroutine to produce a result without actually returning. Can be used for writing
+generators. Currently, this library has no support/usage of `co_yield`, so I won't go into more details
+here.
+
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Qt vs. co_await
+
+One of the best examples where coroutines simplify your code is when dealing with asynchronous
+operations, like network operations.
+
+Let's see how a simple HTTP request would be handled in Qt using the signals/slots mechanism:
+
+```cpp
+void MyClass::fetchData() {
+ auto *nam = new QNetworkAccessManager(this);
+ auto *reply = nam->get(QUrl{QStringLiteral("https://.../api/fetch")});
+ QObject::connect(reply, &QNetworkReply::finished,
+ [reply, nam]() {
+ const auto data = reply->readAll();
+ doSomethingWithData(data);
+ reply->deleteLater();
+ nam->deleteLater();
+ });
+}
+```
+
+Now let's see how the code looks like if we use coroutines:
+
+```cpp
+QCoro::Task<> MyClass::fetchData() {
+ QNetworkReply nam;
+ auto *reply = co_await nam.get(QUrl{QStringLiteral("https://.../api/fetch")});
+ const auto data = reply->readAll();
+ reply->deleteLater();
+ doSomethingWithData(data);
+}
+```
+
+The magic here is the `co_await` keyword which has turned our method `fetchData()`
+into a coroutine and suspended its execution while the network request was running.
+When the request finishes, the coroutine is resumed from where it was suspended and
+continues.
+
+And the best part? While the coroutine is suspended, the Qt event loop runs as usual!
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# More reading
+
+This library is inspired by Lewis Bakers' cppcoro library, which also served as a guide to implementing
+the coroutine machinery, alongside his great series on C++ coroutines:
+
+ * [Coroutine Theory](https://lewissbaker.github.io/2017/09/25/coroutine-theory)
+ * [Understanding Operator co_await](https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await)
+ * [Understanding the promise type](https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type)
+ * [Understanding Symmetric Transfer](https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer)
+
+I can also recommend numerous articles about C++ coroutines by [Raymond Chen on his blog OldNewThink][oldnewthing].
+
+[oldnewthing]: https://devblogs.microsoft.com/oldnewthing/author/oldnewthing
+
--- /dev/null
+#include <QCoroDBus>
+
+QCoro::Task<QString> PlayerControl::nextSong() {
+ // Create a regular QDBusInterface representing the Spotify MPRIS interface
+ QDBusInterface spotifyPlayer{QStringLiteral("org.mpris.MediaPlayer2.spotify"),
+ QStringLiteral("/org/mpris/MediaPlayer2"),
+ QStringLiteral("org.mpris.MediaPlayer2.Player")};
+ // Call CanGoNext DBus method and co_await reply. During that the current coroutine is suspended.
+ const QDBusReply<bool> canGoNext = co_await spotifyPlayer.asyncCall(QStringLiteral("CanGoNext"));
+ // Response has arrived and coroutine is resumed. If the player can go to the next song,
+ // do another async call to do so.
+ if (static_cast<bool>(canGoNext)) {
+ // co_await the call to finish, but throw away the result
+ co_await spotifyPlayer.asyncCall(QStringLiteral("Next"));
+ }
+
+ // Finally, another async call to retrieve new track metadata. Once again, the coroutine
+ // is suspended while we wait for the result.
+ const QDBusReply<QVariantMap> metadata = co_await spotifyPlayer.asyncCall(QStringLiteral("Metadata"));
+ // Since this function uses co_await, it is in fact a coroutine, so it must use co_return in order
+ // to return our result. By definition, the result of this function can be co_awaited by the caller.
+ co_return static_cast<const QVariantMap &>(metadata)[QStringLiteral("xesam:title")].toString();
+}
+
--- /dev/null
+#include <QCoroFuture>
+
+QCoro::Task<> runTask() {
+ // Starts a concurrent task and co_awaits on the returned QFuture. While the task is
+ // running, the coroutine is suspended.
+ const QString value = co_await QtConcurrent::run([]() {
+ QString result;
+ ...
+ // do some long-running computation
+ ...
+ return result;
+ });
+ // When the future has finished, the coroutine is resumed and the result of the
+ // QFuture is returned and stored in `value`.
+
+ // ... now do something with the value
+}
--- /dev/null
+#include <QCoroNetworkReply>
+
+QCoro::Task<> MyClass::fetchData() {
+ // Creates QNetworkAccessManager on stack
+ QNetworkAccessManager nam;
+ // Calls QNetworkAccessManager::get() and co_awaits on the returned QNetworkReply*
+ // until it finishes. The current coroutine is suspended until that.
+ auto *reply = co_await nam.get(QUrl{QStringLiteral("https://.../api/fetch")});
+ // When the reply finishes, the coroutine is resumed and we can access the reply content.
+ const auto data = reply->readAll();
+ // Raise your hand if you never forgot to delete a QNetworkReply...
+ delete reply;
+ doSomethingWithData(data);
+ // Extra bonus: the QNetworkAccessManager is destroyed automatically, since it's on stack.
+}
+
--- /dev/null
+#include <QCoroProcess>
+
+QCoro::Task<QByteArray> listDir(const QString &dirPath) {
+ QProcess basicProcess;
+ auto process = qCoro(basicProcess);
+ qDebug() << "Starting ls...";
+ co_await process.start(QStringLiteral("/bin/ls"), {dirPath});
+ qDebug() << "Ls started, reading directory...";
+
+ co_await process.waitForFinished();
+ qDebug() << "Done";
+
+ return basicProcess.readAll();
+}
+
--- /dev/null
+#include <QCoroTcpServer>
+
+QCoro::Task<> runServer(uint16_t port) {
+ QTcpServer server;
+ server.listen(QHostAddress::LocalHost, port);
+
+ while (server.isListening()) {
+ auto *socket = co_await qCoro(server).waitForNewConnection(10s);
+ if (socket != nullptr) {
+ newClientConnection(socket);
+ }
+ }
+}
+
--- /dev/null
+#include <QCoroTcpSocket>
+
+QCoro::Task<QByteArray> requestDataFromServer(const QString &hostName) {
+ QTcpSocket socket;
+ if (!co_await qCoro(socket).connectToHost(hostName)) {
+ qWarning() << "Failed to connect to the server";
+ co_return QByteArray{};
+ }
+
+ socket.write("SEND ME DATA!");
+
+ QByteArray data;
+ while (!data.endsWith("\r\n.\r\n")) {
+ data += co_await qCoro(socket).readAll();
+ }
+
+ co_return data;
+}
+`
--- /dev/null
+#include <QCoroThread>
+#include <QThread>
+
+#include <memory>
+
+QCoro::Task<void> MainWindow::processData(const QVector<qint64> &data) {
+ std::unique_ptr<QThread> thread(QThread::create([data]() {
+ // Perform some intesive calculation
+ }));
+ thread->start();
+
+ ui->setState(tr("Processing is starting..."));
+ co_await qCoro(thread.get()).waitForStarted();
+ ui->setState(tr("Processing data..."));
+ co_await qCoro(thread.get()).waitForFinished();
+ ui->setState(tr("Processing done."));
+}
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro
+
+C++ Coroutine Library for Qt5 and Qt6
+
+---
+
+## Overview
+
+QCoro is a C++ library that provide set of tools to make use of C++20 coroutines
+in connection with certain asynchronous Qt actions.
+
+Take a look at the example below to see what an amazing thing coroutines are:
+```cpp
+QNetworkAccessManager networkAccessManager;
+// co_await the reply - the coroutine is suspended until the QNetworkReply is finished.
+// While the coroutine is suspended, *the Qt event loop runs as usual*.
+const QNetworkReply *reply = co_await networkAccessManager.get(url);
+// Once the reply is finished, your code resumes here as if nothing amazing has just happened ;-)
+const auto data = reply->readAll();
+```
+
+This library has only one class and one function that the user must be aware of: the class is
+[`QCoro::Task`](reference/coro/task.md) and must be used as a return type for any coroutine that `co_await`s
+a Qt type. The function is [`qCoro()`](reference/coro/coro.md) and it provides coroutine-friendly
+wrappers for Qt types that have multiple asynchronous operations that the user may want to `co_await`
+(for example [`QProcess`](reference/core/qprocess.md)). All the other code (basically everything in the
+`QCoro::detail` namespace) is here to provide the cogs and gears for the C++ coroutine machinery,
+making it possible to use Qt types with coroutines.
+
+The major benefit of using coroutines with Qt types is that it allows writing asynchronous code as if it
+were synchronous and, most importantly, while the coroutine is `co_await`ing, the __Qt event loop runs
+as usual__, meaning that your application remains responsive.
+
+This is a rather experimental library that I started working on to better understand coroutines
+in C++. After reading numerous articles and blog posts about coroutines, it still wasn't exactly
+clear to me how the whole thing works, so I started working on this library to get a better idea
+about coroutines.
+
+## Coroutines
+
+Coroutines are regular functions, except that they can be suspended and resumed again. When
+a coroutine is suspended, it returns sort of a promise to the caller and the caller continues
+executing their code. At some point, the caller can use the newly introduced `co_await` keyword
+to wait for the returned promise to be fulfilled. When that happens, the caller is suspended
+and instead the coroutine is resumed.
+
+This allows writing asynchronous code as if it were synchronous, making it much easier to read
+and understand.
+
+That's not all that coroutines can do, you can read more about it in the 'Coroutines' section
+of this documentation.
+
+## Supported Qt Versions
+
+QCoro supports compiling for both Qt5 and Qt6. Minimum supported versions are:
+
+* Qt5 >= 5.15.2
+* Qt6 >= 6.2.0
+
+Pass `-DUSE_QT_VERSION=5` or `-DUSE_QT_VERSION=6` to CMake to force building QCoro with respective
+major version of Qt. QCoro will default to Qt6 when available and fallback to Qt5 otherwise.
+
+## Supported Compilers
+
+This library requires a compiler that supports the Coroutine TS (obviously). Currently
+GCC, Clang and MSVC are supported.
+
+Officially supported compilers are:
+
+* GCC >= 11 (April 2021)
+* Clang >= 15 (September 2022)
+* MSVC >= 19.40 (Visual Studio 17 2022)
+* AppleClang >= 15.0.0 (Xcode 15.2)
+
+In both GCC and Clang, coroutine support must be explicitly enabled.
+
+### GCC
+
+To enable coroutines support in GCC, add `-fcoroutines` to `CXX_FLAGS`.
+
+CMake:
+```cmake
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines")
+```
+
+Alternatively, just use `qcoro_enable_coroutines()` CMake macro provided by QCoro to set the
+flags automatically.
+
+### Clang
+
+In Clang coroutines are still considered experimental (unlike in GCC).
+Coroutines are enabled by adding `-fcoroutines-ts` to `CMAKE_CXX_FLAGS`.
+
+CMake:
+```cmake
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines-ts")
+```
+
+Alternatively, just use `qcoro_enable_coroutines()` CMake macro provided by QCoro to set the
+flags automatically.
+
+### MSVC
+
+Coroutine support in MSVC is enabled automatically by CMake when C++20 standard is specified
+in `CMAKE_CXX_STANDARD`:
+
+```cmake
+set(CMAKE_CXX_STANDARD 20)
+```
+
--- /dev/null
+def define_env(env):
+
+ @env.macro
+ def doctable(module, include, inherits=None, inheritedBy=[], since=None):
+ def row(th, td):
+ return f"<tr><th>{ th }</th><td>{ td }</td></tr>"
+
+ def inheritsLink(inherits):
+ return f"""<a href="../../{inherits[0]}">{inherits[1]}</a>"""
+
+ out = """<div class="doctable"><table>"""
+ out += row("Module", module)
+ out += row("Include", f"""
+```cpp
+#include <{include}>
+```
+""")
+ out += row("CMake", f"""
+```cpp
+target_link_libraries(myapp QCoro::{module})
+```
+""")
+ out += row("QMake", f"""
+```cpp
+QT += QCoro{module}
+```
+""")
+ if inherits:
+ out += row("Inherits", inheritsLink(inherits))
+
+ if inheritedBy:
+ out += row("Inherited By", ', '.join(sorted(map(inheritsLink, inheritedBy))))
+
+ if since:
+ out += row("Since", since)
+
+ out += "</table></div>"
+ return out
--- /dev/null
+# Blog
+
+{{ blog_content }}
--- /dev/null
+---
+title: QCoro 0.1.0 Release Announcement
+date: "2021-08-16"
+description: I'm happy to announce first release of QCoro, a library that provides C++ coroutine support for Qt.
+---
+
+<!--
+SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.1.0 Release Announcement
+
+I'm happy to announce first release of QCoro, a library that provides C++ coroutine support for Qt.
+
+You can download QCoro 0.1.0 [here][qcoro-release] or check the latest sources on [QCoro GitHub][qcoro-github].
+
+I have talked about QCoro (and C++ coroutines in general) recently at KDE Akademy, you can view the
+[recording of my talk on YouTube][qcoro-youtube].
+
+In general, QCoro provides coroutine support for various asynchronous operations provided by Qt. Since
+Qt doesn't support coroutines by default, QCoro provides the necessary "glue" between native Qt types
+and the C++ coroutine machinery, making it possible to use Qt types with coroutines easily.
+
+QCoro provides coroutine support for asynchronous operations of `QIODevice`, `QNetworkReply`, `QProcess`,
+`QDBusPendingReply`, `QTimer` and more. Take a look at the documentation for detailed description and list
+of all currently supported Qt types.
+
+A brief example from [our documentation][qcoro-docs] that demonstrates how using coroutines makes handling asynchronous
+operations in Qt simpler:
+
+This is a (simplified) example of how we do network requests with Qt normally, using signals and slots:
+```cpp
+QNetworkAccessManager *manager = new QNetworkAccessManager(this);
+QNetworkReply *reply = manager->get(url);
+connect(reply, &QNetworkReply::finished, this,
+ [this, reply]() {
+ const auto data = reply->readAll();
+ doSomethingWithData(data);
+ reply->deleteLater();
+ });
+```
+
+And this is the same code, written using C++ coroutines:
+```cpp
+QNetworkAccessManager networkAccessManager;
+QNetworkReply *reply = co_await networkAccessManager.get(url);
+const auto data = reply->readAll();
+doSomethingWithData(data);
+reply->deleteLater();
+```
+
+The `co_await` keyword here is the key here: it asynchronously waits for the reply to finish. During the wait,
+the execution returns to the caller, which could be the Qt event loop, which means that even if this code *looks*
+synchronous, in fact it won't block the event loop while keeping the code simple to read and understand.
+
+
+[qcoro-release]: https://github.com/danvratil/qcoro/releases/tag/v0.1.0
+[qcoro-github]: https://github.com/danvratil/qcoro
+[qcoro-youtube]: https://www.youtube.com/watch?v=KKVqFqbXJaU&list=PLsHpGlwPdtMq6pJ4mqBeYNWOanjdIIPTJ&index=20
+[qcoro-docs]: https://qcoro.dvratil.cz/
--- /dev/null
+---
+title: QCoro 0.2.0 Release Announcement
+date: "2021-09-08"
+description: >
+ QCoro 0.2.0 has been released. The most visible change is that the library has been modularized,
+ to follow the Qt modules and headers have been cleaned.
+---
+
+<!--
+SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.2.0 Release Announcement
+
+## Library modularity
+
+The code has been reorganized into three modules (and thus three standalone libraries): QCoroCore, QCoroDBus and
+QCoroNetwork. QCoroCore contains the elementary QCoro tools (`QCoro::Task`, `qCoro()` wrapper etc.) and coroutine
+support for some QtCore types. The QCoroDBus module contains coroutine support for types from the QtDBus module
+and equally the QCoroNetwork module contains coroutine support for types from the QtNetwork module. The latter two
+modules are also optional, the library can be built without them. It also means that an application that only uses
+let's say QtNetwork and has no DBus dependency will no longer get QtDBus pulled in through QCoro, as long as it
+only links against `libQCoroCore` and `libQCoroNetwork`. The reorganization will also allow for future
+support of additional Qt modules.
+
+## Headers clean up
+
+The include headers in QCoro we a bit of a mess and in 0.2.0 they all got a unified form. All public header files
+now start with `qcoro` (e.g. `qcorotimer.h`, `qcoronetworkreply.h` etc.), and QCoro also provides CamelCase headers
+now. Thus users should simply do `#include <QCoroTimer>` if they want coroutine support for `QTimer`.
+
+The reorganization of headers makes QCoro 0.2.0 incompatible with previous versions and any users of QCoro will
+have to update their `#include` statements. I'm sorry about this extra hassle, but with this brings much needed
+sanity into the header organization and naming scheme.
+
+## Docs update
+
+The documentation has been updated to reflect the reorganization as well as some internal changes. It should be
+easier to understand now and hopefully will make it easier for users to start with QCoro now.
+
+## Internal API cleanup and code de-duplication
+
+Historically, certain types types which can be directly `co_await`ed with QCoro, for instance `QTimer` has their
+coroutine support implemented differently than types that have multiple asynchronous operations and thus have
+a coroutine-friendly wrapper classes (like `QIODevice` and it's `QCoroIODevice` wrapper). In 0.2.0 I have unified
+the code so that even the coroutine support for simple types like `QTimer` are implemented through wrapper classes
+(so there's `QCoroTimer` now)
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.2.0)
--- /dev/null
+---
+title: QCoro 0.4.0 Release Announcement
+date: "2022-01-06"
+description: >
+ Major highlights of this release are co-installability of Qt5 and Qt6 builds of QCoro,
+ complete re-work of CMake configuration and support for compiling QCoro with the Clang
+ compiler against GNU's libstdc++.
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.4.0 Release Announcement
+
+Major highlights in this release:
+
+* Co-installability of Qt5 and Qt6 builds of QCoro
+* Complete re-work of CMake configuration
+* Support for compiling QCoro with Clang against libstdc++
+
+## Co-installability of Qt5 and Qt6 builds of QCoro
+
+This change mostly affects packagers of QCoro. It is now possible to install both Qt5 and Qt6 versions
+of QCoro alongside each other without conflicting files. The shared libraries now contain the Qt version
+number in their name (e.g. `libQCoro6Core.so`) and header files are also located in dedicated subdirectories
+(e.g. `/usr/include/qcoro6/{qcoro,QCoro}`). User of QCoro should not need to do any changes to their codebase.
+
+## Complete re-work of CMake configuration
+
+This change affects users of QCoro, as they will need to adjust CMakeLists.txt of their projects. First,
+depending on whether they want to use Qt5 or Qt6 version of QCoro, a different package must be used.
+Additionally, list of QCoro components to use must be specified:
+
+```
+find_package(QCoro5 REQUIRED COMPONENTS Core Network DBus)
+```
+
+Finally, the target names to use in `target_link_libraries` have changed as well:
+
+* `QCoro::Core`
+* `QCoro::Network`
+* `QCoro::DBus`
+
+The version-less `QCoro` namespace can be used regardless of whether using Qt5 or Qt6 build of QCoro.
+`QCoro5` and `QCoro6` namespaces are available as well, in case users need to combine both Qt5 and Qt6
+versions in their codebase.
+
+This change brings QCoro CMake configuration system to the same style and behavior as Qt itself, so it
+should now be easier to use QCoro, especially when supporting both Qt5 and Qt6.
+
+## Support for compiling QCoro with Clang against libstdc++
+
+Until now, when the Clang compiler was detected, QCoro forced usage of LLVM's libc++ standard library.
+Coroutine support requires tight co-operation between the compiler and standard library. Because Clang
+still considers their coroutine support experimental it expects all coroutine-related types in standard
+library to be located in `std::experimental` namespace. In GNU's libstdc++, coroutines are fully supported
+and thus implemented in the `std` namespace. This requires a little bit of extra glue, which is now in place.
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.4.0)
--- /dev/null
+---
+title: QCoro 0.5.0 Release Announcement
+date: "2022-04-25"
+description: >
+ Major feature in the 0.5.0 release are .then() continuations for Task<T>, all asynchronous
+ operations in QCoro returning Task<T> and many operations have gained support for optional
+ timeout argument. Last but not least, coroutine-friendly wrapper for QThread has been
+ introduced.
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.5.0 Release Announcement
+
+Major features:
+
+* .then() continuation for `Task<T>`
+* All asynchronous operations now return `Task<T>`
+* Timeouts for many operations
+* Support for `QThread`
+
+## .then() continuation for Task<T>
+
+Sometimes it's not possible to `co_await` a coroutine - usually because you need to integrate with a 3rd party code
+that is not coroutine-ready. A good example might be implementing `QAbstractItemModel`, where none of the virtual
+methods are coroutines and thus it's not possible to use `co_await` in them.
+
+To still make it possible to all coroutines from such code, `QCoro::Task<T>` now has a new method: `.then()`,
+which allows attaching a continuation callback that will be invoked by QCoro when the coroutine represented
+by the `Task` finishes.
+
+```cpp
+void notACoroutine() {
+ someCoroutineReturningQString().then([](const QString &result) {
+ // Will be invoked when the someCoroutine() finishes.
+ // The result of the coroutine is passed as an argument to the continuation.
+ });
+}
+```
+
+The continuation itself might be a coroutine, and the result of the `.then()` member function is again a `Task<R>`
+(where `R` is the return type of the continuation callback), so it is possible to chain multiple continuations
+as well as `co_await`ing the entire chain.
+
+## All asynchronous operations now return `Task<T>`
+
+Up until now each operation from the QCoro wrapper types returned a special awaitable - for example,
+`QCoroIODevice::read()` returned `QCoro::detail::QCoroIODevice::ReadOperation`. In most cases users of QCoro do
+not need to concern themselves with that type, since they can still directly `co_await` the returned awaitable.
+
+However, it unnecessarily leaks implementation details of QCoro into public API and it makes it harded to return
+a coroutine from a non-coroutine function.
+
+As of QCoro 0.5.0, all the operations now return `Task<T>`, which makes the API consistent. As a secondary effect,
+all the operations can have a chained continuation using the `.then()` continuation, as described above.
+
+## Timeout support for many operations
+
+Qt doesn't allow specifying timeout for many operations, because they are typically non-blocking. But the timeout
+makes sense in most QCoro cases, because they are combination of wait + the non-blocking operation. Let's take
+`QIODevice::read()` for example: the Qt version doesn't have any timeout, because the call will never block - if
+there's nothing to read, it simply returns an empty `QByteArray`.
+
+On the other hand, `QCoroIODevice::read()` is an asynchronous operation, because under to hood, it's a coroutine
+that asynchronously calls a sequence of
+
+```cpp
+device->waitForReadyRead();
+device->read();
+```
+
+Since `QIODevice::waitForReadyRead()` takes a timeout argument, it makes sense for `QCoroIODevice::read()`
+to also take (an optional) timeout argument. This and many other operations have gained support for timeout.
+
+## Support for `QThread`
+
+It's been a while since I added a new wrapper for a Qt class, so QCoro 0.5.0 adds wrapper for `QThread`. It's
+now possible to `co_await` thread start and end:
+
+```cpp
+std::unique_ptr<QThread> thread(QThread::create([]() {
+ ...
+});
+ui->setLabel(tr("Starting thread...");
+thread->start();
+co_await qCoro(thread)->waitForStarted();
+ui->setLabel(tr("Calculating..."));
+co_await qCoro(thread)->waitForFinished();
+ui->setLabel(tr("Finished!"));
+```
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.5.0)
+
+<hr>
+
+Thanks to everyone who contributed to QCoro!
--- /dev/null
+---
+title: QCoro 0.6.0 Release Announcement
+date: "2022-07-09"
+description: >
+ This release brings several major new featrues alongside a bunch of bufixes and
+ improvements inside QCoro. The four major features are generators support,
+ new QCoroWebSocket module, deprecation of task.h header, and introduction of
+ clang-cl and apple-clang support.
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.6.0 Release Announcement
+
+This release brings several major new features alongside a bunch of bugfixes and
+improvements inside QCoro.
+
+The four major features are:
+
+* Generator support
+* New QCoroWebSockets module
+* Deprecated task.h
+* Clang-cl and apple-clang support
+
+🎉 Starting with 0.6.0 I no longer consider this library to be experimental
+(since clearly the experiment worked :-)) and its API to be stable enough for
+general use. 🎉
+
+As always, big thank you to everyone who report issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Generator support
+
+Unlike regular functions (or `QCoro::Task<>`-based coroutines) which can only ever
+produce at most single result (through `return` or `co_return` statement), generators
+can yield results repeatedly without terminating. In QCoro we have two types of generators:
+synchronous and asynchronous. Synchronous means that the generator produces each value
+synchronously. In QCoro those are implemented as `QCoro::Generator<T>`:
+
+```cpp
+// A generator that produces a sequence of numbers from 0 to `end`.
+QCoro::Generator<int> sequence(int end) {
+ for (int i = 0; i <= end; ++i) {
+ // Produces current value of `i` and suspends.
+ co_yield i;
+ }
+ // End the iterator
+}
+
+int sumSequence(int end) {
+ int sum = 0;
+ // Loops over the returned Generator, resuming the generator on each iterator
+ // so it can produce a value that we then consume.
+ for (int value : sequence(end)) {
+ sum += value;
+ }
+ return sum;
+}
+```
+
+The `Generator` interface implements `begin()` and `end()` methods which produce an
+iterator-like type. When the iterator is incremented, the generator is resumed to yield
+a value and then suspended again. The iterator-like interface is not mandated by the C++
+standard (the C++ standard provides no requirements for generators), but it is an
+intentional design choice, since it makes it possible to use the generators with existing
+language constructs as well as standard-library and Qt features.
+
+You can find more details about synchronous generators in the [`QCoro::Generator<T>`
+documentation](https://qcoro.dvratil.cz/reference/coro/generator/).
+
+Asynchronous generators work in a similar way, but they produce value asynchronously,
+that is the result of the generator must be `co_await`ed by the caller.
+
+```cpp
+QCoro::AsyncGenerator<QUrl> paginator(const QUrl &baseUrl) {
+ QUrl pageUrl = baseUrl;
+ Q_FOREVER {
+ pageUrl = co_await getNextPage(pageUrl); // co_awaits next page URL
+ if (pageUrl.isNull()) { // if empty, we reached the last page
+ break; // leave the loop
+ }
+ co_yield pageUrl; // finally, yield the value and suspend
+ }
+ // end the generator
+}
+
+QCoro::AsyncGenerator<QString> pageReader(const QUrl &baseUrl) {
+ // Create a new generator
+ auto generator = paginator(baseUrl);
+ // Wait for the first value
+ auto it = co_await generator.begin();
+ auto end = generator.end();
+ while (it != end) { // while the `it` iterator is valid...
+ // Asynchronously retrieve the page content
+ const auto content = co_await fetchPageContent(*it);
+ // Yield it to the caller, then suspend
+ co_yield content;
+ // When resumed, wait for the paginator generator to produce another value
+ co_await ++it;
+ }
+}
+
+QCoro::Task<> downloader(const QUrl &baseUrl) {
+ int page = 1;
+ // `QCORO_FOREACH` is like `Q_FOREACH` for asynchronous iterators
+ QCORO_FOREACH(const QString &page, pageReader(baseUrl)) {
+ // When value is finally produced, write it to a file
+ QFile file(QStringLiteral("page%1.html").arg(page));
+ file.open(QIODevice::WriteOnly);
+ file.write(page);
+ ++page;
+ }
+}
+```
+
+Async generators also have `begin()` and `end()` methods which provide an asynchronous
+iterator-like types. For one, the `begin()` method itself is a coroutine and must be
+`co_await`ed to obtain the initial iterator. The increment operation of the iterator
+must then be `co_await`ed as well to obtain the iterator for the next value.
+Unfortunately, asynchronous iterator cannot be used with ranged-based for loops, so
+QCoro provides [`QCORO_FOREACH` macro](https://qcoro.dvratil.cz/reference/coro/asyncgenerator/#qcoro_foreach) to make using asynchronous generators simpler.
+
+Read the [documentation for `QCoro::AsyncGenerator<T>`](https://qcoro.dvratil.cz/reference/coro/asyncgenerator) for more details.
+
+## New QCoroWebSockets module
+
+The QCoroWebSockets module provides QCoro wrappers for `QWebSocket` and `QWebSocketServer`
+classes to make them usable with coroutines. Like the other modules, it's a standalone
+shared or static library that you must explicitly link against in order to be able to use
+it, so you don't have to worry that QCoro would pull websockets dependency into your
+project if you don't want to.
+
+```cpp
+QCoro::Task<> ChatApp::handleNotifications(const QUrl &wsServer) {
+ if (!co_await qCoro(mWebSocket).open(wsServer)) {
+ qWarning() << "Failed to open websocket connection to" << wsServer << ":" << mWebSocket->errorString();
+ co_return;
+ }
+ qDebug() << "Connected to" << wsServer;
+
+ // Loops whenever a message is received until the socket is disconnected
+ QCORO_FOREACH(const QString &rawMessage, qCoro(mWebSocket).textMessages()) {
+ const auto message = parseMessage(rawMessage);
+ switch (message.type) {
+ case MessageType::ChatMessage:
+ handleChatMessage(message);
+ break;
+ case MessageType::PresenceChange:
+ handlePresenceChange(message);
+ break;
+ case MessageType::Invalid:
+ qWarning() << "Received an invalid message:" << message.error;
+ break;
+ }
+ }
+}
+```
+The `textMessages()` methods returns an asynchronous generator, which yields the message
+whenever it arrives. The messages are received and enqueued as long as the generator
+object exists. The difference between using a generator and just `co_await`ing the next
+emission of the `QWebSocket::textMessage()` signal is that the generator holds a connection
+to the signal for its entire lifetime, so no signal emission is lost. If we were only
+`co_await`ing a singal emission, any message that is received before we start `co_await`ing
+again after handling the current message would be lost.
+
+You can find more details about the `QCoroWebSocket` and `QCoroWebSocketSever`
+in the [QCoro's websocket module documentation](https://qcoro.dvratil.cz/reference/websockets/).
+
+You can build QCoro without the WebSockets module by passing `-DQCORO_WITH_QTWEBSOCKETS=OFF`
+to CMake.
+
+## Deprecated tasks.h header
+
+The `task.h` header and it's camelcase variant `Task` been deprecated in QCoro 0.6.0
+in favor of `qcorotask.h` (and `QCoroTask` camelcase version). The main reasons are to
+avoid such a generic name in a library and to make the name consistent with the rest of
+QCoro's public headers which all start with `qcoro` (or `QCoro`) prefix.
+
+The old header is still present and fully functional, but including it will produce a
+warning that you should port your code to use `qcorotask.h`. You can suppress the warning
+by defining `QCORO_NO_WARN_DEPRECATED_TASK_H` in the compiler definitions:
+
+CMake:
+```cmake
+add_compiler_definitions(QCORO_NO_WARN_DEPRECATED_TASK_H)
+```
+
+QMake
+```qmake
+DEFINES += QCORO_NO_WARN_DEPRECATED_TASK_H
+```
+
+The header file will be removed at some point in the future, at latest in the 1.0 release.
+
+You can also pass `-DQCORO_DISABLE_DEPRECATED_TASK_H=ON` to CMake when compiling QCoro
+to prevent it from installing the deprecated task.h header.
+
+## Clang-cl and apple-clang support
+
+The clang compiler is fully supported by QCoro since 0.4.0. This version of QCoro
+intruduces supports for clang-cl and apple-clang.
+
+Clang-cl is a compiler-driver that provides MSVC-compatible command line options,
+allowing to use clang and LLVM as a drop-in replacement for the MSVC toolchain.
+
+Apple-clang is the official build of clang provided by Apple on MacOS, which may be
+different from the upstream clang releases.
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.6.0)
--- /dev/null
+---
+title: QCoro 0.7.0 Release Announcement
+date: "2022-11-16"
+description: >
+ Initial QML support, new QCoro::connect() helper and many small fixes - all of this and more in QCoro 0.7.0.
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.7.0 Release Announcement
+
+The major new feature in this release is initial QML support, contributed by
+Jonah Brüchert. Jonah also contributed `QObject::connect` helper and
+a coroutine version of QQuickImageProvider. As always, this release includes
+some smaller enhancements and bugfixes, you can find a full list of them
+on the Github release page.
+
+As always, big thank you to everyone who report issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Initial QML Support
+
+Jonah Brüchert has contributed initial support for QML. Unfortunately, we
+cannot extend the QML engine to support the `async` and `await` keywords from
+ES8, but we can make it possible to set a callback from QML that should be
+called when the coroutine finishes.
+
+The problem with `QCoro::Task` is that it is a template class so it cannot be
+registered into the QML type system and used from inside QML. The solution
+that Jonach has come up with is to introduce `QCoro::QmlTask` class, which
+can wrap any awaitable (be it `QCoro::Task` or any generic awaitable type)
+and provides a `then()` method that can be called from QML and that takes
+a JavaScript function as its only argument. The function will be invoked by
+`QCoro::QmlTask` when the wrapped awaitable has finished.
+
+The disadvantage of this approach is that in order to expose a class that
+uses `QCoro::Task<T>` as return types of its member functions into QML, we
+need to create a wrapper class that converts those return types to
+`QCoro::QmlTask`.
+
+Luckily, we should be able to provide a smoother user experience when using
+QCoro in QML for Qt6 in a future QCoro release.
+
+```cpp
+class QmlCoroTimer: public QObject {
+ Q_OBJECT
+public:
+ explicit QmlCoroTimer(QObject *parent = nullptr)
+ : QObject(parent)
+ {}
+
+ Q_INVOCABLE QCoro::QmlTask start(int milliseconds) {
+ // Implicitly wraps QCoro::Task<> into QCoro::QmlTask
+ return waitFor(milliseconds);
+ }
+
+private:
+ // A simple coroutine that co_awaits a timer timeout
+ QCoro::Task<> waitFor(int milliseconds) {
+ QTimer timer;
+ timer.start(milliseconds);
+ co_await timer;
+ }
+};
+
+...
+QCoro::Qml::registerTypes();
+qmlRegisterType<QmlCoroTimer>("cz.dvratil.qcoro.example", 0, 1);
+```
+
+```qml
+import cz.dvratil.qcoro.example 1.0
+
+Item {
+
+ QmlCoroTimer {
+ id: timer
+ }
+
+ Component.onCompleted: () {
+ // Attaches a callback to be called when the QmlCoroTimer::waitFor()
+ // coroutine finishes.
+ timer.start(1000).then(() => {
+ console.log("1 second elapsed!");
+ });
+ }
+}
+```
+
+Read the [documentation for `QCoro::QmlTask`](https://qcoro.dvratil.cz/reference/qml/qmltask) for more details.
+
+
+## QCoro::connect Helper
+
+The `QCoro::connect()` helper is similar to `QObject::connect()` - except you
+you pass in a `QCoro::Task<T>` instead of a sender and signal pointers. While
+using the `.then()` continuation can achieve similar results, the main
+difference is that `QCoro::connect()` takes a pointer to a context (receiver)
+QObject. If the receiver is destroyed before the connected `QCoro::Task<T>`
+finishes, the slot is not invoked.
+
+```cpp
+void MyClass::buttonClicked() {
+ QCoro::Task<QByteArray> task = sendNetworkRequest();
+ // If this object is deleted before the `task` completes,
+ // the slot is not invoked.
+ QCoro::connect(std::move(task), this, &handleNetworkReply);
+}
+```
+
+See the [QCoro documentation](https://qcoro.dvratil.cz/reference/coro/task/#interfacing-with-synchronous-functions) for more details.
+
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.7.0)
--- /dev/null
+---
+title: QCoro 0.8.0 Release Announcement
+date: "2023-01-31"
+description: >
+ Improved QCoro::waitFor(), new sleepFor() and sleepUntil() helper functions and
+ improved thread support.
+---
+
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.8.0 Release Announcement
+
+This is a rather small release with only two new features and one small improvement.
+
+Big thank you to [Xstrahl Inc.](https://xstrahl.com) who sponsored development of
+new features included in this release and of QCoro in general.
+
+And as always, thank you to everyone who reported issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Improved `QCoro::waitFor()`
+
+Up until this version, `QCoro::waitFor()` was only usable for `QCoro::Task<T>`.
+Starting with QCoro 0.8.0, it is possible to use it with any type that satisfies
+the `Awaitable` concept. The concept has also been fixed to satisfies not just
+types with the `await_resume()`, `await_suspend()` and `await_ready()` member functions,
+but also types with member `operator co_await()` and non-member `operator co_await()`
+functions.
+
+## `QCoro::sleepFor()` and `QCoro::sleepUntil()`
+
+Working both on QCoro codebase as well as some third-party code bases using QCoro
+it's clear that there's a usecase for a simple coroutine that will sleep for
+specified amount of time (or until a specified timepoint). It is especially useful
+in tests, where simulating delays, especially in asynchronous code is common.
+
+Previously I used to create small coroutines like this:
+
+```cpp
+QCoro::Task<> timer(std::chrono::milliseconds timeout) {
+ QTimer timer;
+ timer.setSingleShot(true);
+ timer.start(timeout);
+ co_await timer;
+}
+```
+
+Now we can do the same simply by using `QCoro::sleepFor()`.
+
+Read the [documentation for `QCoro::sleepFor()`](https://qcoro.dvratil.cz/reference/core/qtimer/#qcorosleepfor)
+and [`QCoro::sleepUntil()`](https://qcoro.dvratil.cz/reference/core/qtimer/#qcorosleepuntil) for more details.
+
+## `QCoro::moveToThread()`
+
+A small helper coroutine that allows a piece of function to be executed in the context
+of another thread.
+
+```cpp
+void App::runSlowOperation(QThread *helperThread) {
+ // Still on the main thread
+ ui->statusLabel.setText(tr("Running"));
+
+ const QString input = ui->userInput.text();
+
+ co_await QCoro::moveToThread(helperThread);
+ // Now we are running in the context of the helper thread, the main thread is not blocked
+
+ // It is safe to use `input` which was created in another thread
+ doSomeComplexCalculation(input);
+
+ // Move the execution back to the main thread
+ co_await QCoro::moveToThread(this->thread());
+ // Runs on the main thread again
+ ui->statusLabel.setText(tr("Done"));
+}
+```
+
+Read the [documentation for `QCoro::moveToThread`](https://qcoro.dvratil.cz/reference/core/qthread#qcoromovetothread) for more details.
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.8.0)
+
--- /dev/null
+---
+title: QCoro 0.9.0 Release Announcement
+date: "2023-04-27"
+description: >
+ Important bugfixes, huge improvement to QML support and a brand new Test module.
+---
+
+<!--
+SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.9.0 Release Announcement
+
+Another smallish release with just a few new features, but quite important bugfixes.
+
+As always, thank you to everyone who reported issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Enhanced QML Support
+
+Jonah Brüchert has improved the QML support by introducing a declarative API to await
+a task result.
+
+Previously a task result could have only been obtained inside a JavaScript function:
+
+```qml
+Label {
+ id: usernameLabel
+ Component.onCompleted: {
+ api.getUserName().then(function(result) {
+ usernameLabel.text = result
+ })
+ }
+}
+```
+
+With QCoro 0.9.0 you can use the new declarative API to use the result of an asynchronous
+task in a property binding:
+
+```qml
+Label {
+ id: usernameLabel
+ text: api.getUserName().await("Loading...").value
+}
+```
+
+The `Label` will now show the string "Loading..." while the asynchronous task is running
+and will automatically change to the result of the task once if finishes.
+
+You can check the [`QCoro::QmlTask` documentation][qcoro-qmltask-docs] for more details.
+
+## QCoroTest Module
+
+Yet another release with a new QCoro module! This time it's for tests! The QCoroTest
+module contains macros (e.g., `QCORO_VERIFY`, `QCORO_COMPARE`, ...) that are basically
+identical to their QtTest counteparts with the only difference being that they can be
+used inside a coroutine.
+
+We already had this macros inside QCoro test suite almost since the very beginning
+and realized they can be useful to our users as well, since they will likely want
+to have unittests for their coroutine code.
+
+In some of the next releases we would like to add a little bit more infrastructure
+to make writing unittests for coroutines with QtTest even easier.
+
+Check the [`QCoroTest` module documentation][qcoro-test-docs] with a full list of
+the test macros.
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.9.0)
+
+[qcoro-qmltask-docs]: https://qcoro.dvratil.cz/reference/qml/qmltask/
+[qcoro-test-docs]: https://qcoro.dvratil.cz/reference/test/
+
--- /dev/null
+---
+title: QCoro 0.10.0 Release Announcement
+date: "2023-12-05"
+description: >
+ Improved signal/slots features and compatibility and bug fixes.
+---
+
+<!--
+SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.10.0 Release Announcement
+
+Thank you to everyone who reported issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Support for awaiting Qt signals with QPrivateSignal
+
+Qt has a feature where signals can be made "private" (in the sense that only class
+that defines the signal can emit it) by appending `QPrivateSignal` argument to the
+signal method:
+
+```cpp
+class MyObject : public QObject {
+ Q_OBJECT
+...
+Q_SIGNALS:
+ void error(int code, const QString &message, QPrivateSignal);
+};
+```
+
+`QPrivateSignal` is a type that is defined inside the `Q_OBJECT` macro, so it's
+private and as such only `MyObject` class can emit the signal, since only `MyObject`
+can instantiate `QPrivateSignal`:
+
+```cpp
+void MyObject::handleError(int code, const QString &message)
+{
+ Q_EMIT error(code, message, QPrivateSignal{});
+}
+```
+
+QCoro has a feature that makes it possible to `co_await` a signal emission and
+returns the signals arguments as a tuple:
+
+```cpp
+
+MyObject myObject;
+const auto [code, message] = co_await qCoro(&myObject, &MyObject::handleError);
+```
+
+While it was possible to `co_await` a "private" signal previously, it would get
+return the `QPrivateSignal` value as an additional value in the result tuple
+and on some occasions would not compile at all.
+
+In QCoro 0.10, we can detect the `QPrivateSignal` argument and drop it inside QCoro
+so that it does not cause trouble and does not clutter the result type.
+
+Achieving this wasn't simple, as it's not really possible to detect the type (because
+it's private), e.g. code like this would fail to compile, because we are not allowed
+to refer to `Obj::QPrivateSignal`, since that type is private to `Obj`.
+
+```cpp
+template<typename T, typename Obj>
+constexpr bool is_qprivatesignal = std::same_as_v<T, typename Obj::QPrivateSignal>;
+```
+
+After many different attempts we ended up abusing `__PRETTY_FUNCTION__`
+(and `__FUNCSIG__` on MSVC) and checking whether the function's name contains
+`QPrivateSignal` string in the expected location. It's a whacky hack, but hey - if it
+works, it's not stupid :). And thanks to improvements in compile-time evaluation in
+C++20, the check is evaluated completely at compile-time, so there's no runtime
+overhead of obtaining current source location and doing string comparisons.
+
+## Source Code Reorganization (again!)
+
+Big part of QCoro are template classes, so there's a lot of code in headers. In my
+opinion, some of the files (especially qcorotask.h) were getting hard to read and
+navigate and it made it harder to just see the API of the class (like you get
+with non-template classes), which is what users of a library are usually most
+interested in.
+
+Therefore I decided to move definitions into separated files, so that they don't
+clutter the main include files.
+
+This change is completely source- and binary-compatible, so QCoro users don't have
+to make any changes to their code. The only difference is that the main QCoro
+headers are much prettier to look at now.
+
+## Bugfixes
+
+* `QCoro::waitFor()` now re-throws exceptions ([#172][issue172], Daniel Vrátil)
+* Replaced deprecated `QWebSocket::error` with `QWbSocket::errorOccured` in QCoroWebSockets module ([#174][pr174], Marius P)
+* Fix `QCoro::connect()` not working with lambdas ([#179][pr179], Johan Brüchert)
+* Fix library name postfix for qmake compatibilty ([#192][pr192], Shantanu Tushar)
+* Fix `std::coroutine_traits isn't a class template` error with LLVM 16 ([#196][pr196], Rafael Sadowski)
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.10.0)
+
+[issue172]: https://github.com/danvratil/qcoro/issues/172
+[pr174]: https://github.com/danvratil/qcoro/pulls/174
+[pr179]: https://github.com/danvratil/qcoro/pulls/179
+[pr192]: https://github.com/danvratil/qcoro/pulls/192
+[pr196]: https://github.com/danvratil/qcoro/pulls/196
+
--- /dev/null
+---
+title: QCoro 0.11.0 Release Announcement
+date: "2024-10-04"
+description: >
+ LazyTask and many bugfixes
+---
+
+<!--
+SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.11.0 Release Announcement
+
+A long over-due release which has accumulated a bunch of bugfixes but also some
+fancy new features...read on!
+
+As always, big thanks to everyone who reported issues and contributed to QCoro.
+Your help is much appreciated!
+
+## `QCoro::LazyTask<T>`
+
+The biggest new features in this release is the brand-new [`QCoro::LazyTask<T>`][qcoro-lazytask].
+It's a new return type that you can use for your coroutines. It differs from `QCoro::Task<T>`
+in that, as the name suggest, the coroutine is evaluated lazily. What that means is when
+you call a coroutine that returns `LazyTask`, it will return imediately without executing
+the body of the coroutine. The body will be executed only once you `co_await` on the returned
+`LazyTask` object.
+
+This is different from the behavior of `QCoro::Task<T>`, which is eager, meaning that it will
+start executing the body immediately when called (like a regular function call).
+
+```cpp
+QCoro::LazyTask<int> myWorker()
+{
+ qDebug() << "Starting worker";
+ co_return 42;
+}
+
+QCoro::Task<> mainCoroutine()
+{
+ qDebug() << "Creating worker";
+ const auto task = myWorker();
+ qDebug() << "Awaiting on worker";
+ const auto result = co_await task;
+ // do something with the result
+}
+```
+
+This will result in the following output:
+
+```plain
+mainCoroutine(): Creating worker
+mainCoroutine(): Awaiting on worker
+myWorker(): Starting worker
+```
+
+If `myWorker()` were a `QCoro::Task<T>` as we know it, the output would look like this:
+
+```plain
+mainCoroutine(): Creating worker
+myWorker(): Starting worker
+mainCoroutine(): Awaiting on worker
+```
+
+The fact that the body of a `QCoro::LazyTask<T>` coroutine is only executed when `co_await`ed has one
+very important implication: **it must not be used for Qt slots**, `Q_INVOKABLE`s or, in general, for any
+coroutine that may be executed directly by the Qt event loop. The reason is, that the Qt event loop
+is not aware of coroutines (or QCoro), so it will never `co_await` on the returned `QCoro::LazyTask`
+object - which means that the code inside the coroutine would never get executed. This is the
+reason why the good old `QCoro::Task<T>` is an eager coroutine - to ensure the body of the coroutine
+gets executed even when called from the Qt event loop and not `co_await`ed.
+
+For more details, see the [documentation of `QCoro::LazyTask<T>`][qcoro-lazytask].
+
+## Defined Semantics for Awaiting Default-Constructed and Moved-From Tasks
+
+This is something that wasn't clearely defined until now (both in the docs and in the code), which is
+what happens when you try to `co_await` on a default-constructed `QCoro::Task<T>` (or `QCoro::LazyTask<T>`):
+
+```cpp
+co_await QCoro::Task<>(); // will hang indefinitely!
+```
+
+Previously this would trigger a `Q_ASSERT` in debug build and most likely a crash in production build.
+Starting with QCoro 0.11, awaiting such task will print a `qWarning()` and will hang indefinitely.
+
+The same applies to awaiting a moved-from task, which is identical to a default-constructed task:
+
+```cpp
+QCoro::LazyTask<int> task = myTask();
+handleTask(std::move(task));
+
+co_await task; // will hang indefinitely!`
+```
+
+## Compiler Support
+
+We have dropped official support for older compilers. Since QCoro 0.11, the officially supported compilers are:
+
+* GCC >= 11
+* Clang >= 15
+* MSVC >= 19.40 (Visual Studio 17 2022)
+* AppleClang >= 15 (Xcode 15.2)
+
+QCoro might still compile or work with older versions of those compilers, but we no longer test it and
+do not guarantee that it will work correctly.
+
+The reason is that coroutine implementation in older versions of GCC and clang were buggy and behaved differently
+than they do in newer versions, so making sure that QCoro behaves correctly across wide range of compilers was
+getting more difficult as we implemented more and more complex and advanced features.
+
+## Other Features and Changes
+
+A coroutine-friendly version of [`QFuture::takeResult()`][qtdoc-qfuture-takeresult] is now available in the
+form of [`QCoroFuture::takeResult()`][qcorofuture-takeresult] when building QCoro against Qt 6 ([#217][issue217]).
+
+`QCoro::waitFor(QCoro::Task<T>)` no longer requires that the task return type `T` is default-constructible ([#223][pr223], Joey Richey)
+
+## Bugfixes
+
+* Suppress Clang error when building against Android NDK <= 25 ([#204][issue204], Daniel Vrátil)
+* Fixed missing QtGui dependency in QCoroQuick module ([#209][pr209], Andreas Sturmlechner)
+* Fixed `QCoroIODevice::write()` always returning 0 instead of bytes written ([#211][issue211], Daniel Vrátil)
+* Fixed unchecked `std::optional` access in `QCoroIODevice::write`
+* Fixed awaiting on signal emission with `qCoro()` would resume the awaiter in the sender's thread context ([#213][issue213], Daniel Vrátil)
+* Fixed build wilth clang 18 due to missing `#include <exception>` ([#220][pr220], Micah Terhaar)
+* Fixed crash when `QNetworkAccessManager` is destroyed from a coroutine awaiting on a network reply ([#231][issue231], Daniel Vrátil)
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.11.0)
+
+## Support
+
+If you enjoy using QCoro, consider supporting its development on [GitHub Sponsors][github-sponsors] or buy me a coffee
+on [Ko-fi][kofi] (after all, more coffee means more code, right?).
+
+
+[issue231]: https://github.com/danvratil/qcoro/issues/231
+[issue217]: https://github.com/danvratil/qcoro/issues/217
+[issue213]: https://github.com/danvratil/qcoro/issues/213
+[issue211]: https://github.com/danvratil/qcoro/issues/211
+[issue204]: https://github.com/danvratil/qcoro/issues/204
+[pr223]: https://github.com/danvratil/qcoro/pulls/223
+[pr220]: https://github.com/danvratil/qcoro/pulls/220
+[pr209]: https://github.com/danvratil/qcoro/pulls/209
+
+[qtdoc-qfuture-takeresult]: https://doc.qt.io/qt-6/qfuture.html#takeResult
+[qcorofuture-takeresult]: ../../reference/core/qfuture.md#takeResult
+[qcoro-lazytask]: ../../reference/coro/lazytask.md
+
+[github-sponsors]: https://github.com/sponsors/danvratil
+[kofi]: https://ko-fi.com/danvratil
--- /dev/null
+---
+title: QCoro 0.12.0 Release Announcement
+date: "2025-04-03"
+description: >
+ Qt 6.9 and 6.10 support and maintenance
+---
+
+<!--
+SPDX-FileCopyrightText: 2025 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro 0.12.0 Release Announcement
+
+This release brings support Qt 6.9 and dev and a bunch of bugfixes - so mostly
+a maintenance release. No new features, this time.
+
+As always, big thanks to everyone who reported issues and contributed to QCoro.
+Your help is much appreciated!
+
+## Bugfixes
+
+* Fixed typo in QNetworkReply example ([#257][pr257], Mikhail Yatsenko)
+* Don't discard result from QFile::open() in ([#261][pr261], Nicolas Fella)
+* Fixed missing co_await in QIODevice documentation ([#264][pr264], rsp4jack)
+* Fixed MSVC build due to language-extension-token warning ([#266][pr266], DeveloperPaul123)
+* Fixed infinite recursion with debug builds on libstd++ ([#268][pr268], Jared Van Bortel)
+* Fixed QCoro's compile definitions leaking to user code ([#267][pr267], [#275][pr275], Daniel Vrátil)
+* Fixed build against Qt 6.9 ([#276][pr276], Nicolas Fella)
+
+## Full changelog
+
+[See changelog on Github](https://github.com/danvratil/qcoro/releases/tag/v0.12.0)
+
+## Support
+
+If you enjoy using QCoro, consider supporting its development on [GitHub Sponsors][github-sponsors] or buy me a coffee
+on [Ko-fi][kofi] (after all, more coffee means more code, right?).
+
+[pr257]: https://github.com/qcoro/qcoro/pull/257
+[pr261]: https://github.com/qcoro/qcoro/pull/261
+[pr264]: https://github.com/qcoro/qcoro/pull/264
+[pr266]: https://github.com/qcoro/qcoro/pull/266
+[pr267]: https://github.com/qcoro/qcoro/pull/267
+[pr268]: https://github.com/qcoro/qcoro/pull/268
+[pr275]: https://github.com/qcoro/qcoro/pull/275
+[pr276]: https://github.com/qcoro/qcoro/pull/276
+
+[github-sponsors]: https://github.com/sponsors/danvratil
+[kofi]: https://ko-fi.com/danvratil
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Core Module
+
+The `Core` module contains coroutine-friendly wrapper for
+[QtCore][qtdoc-qtcore] classes.
+
+## CMake Usage
+
+```
+find_package(QCoro6 COMPONENTS Core)
+
+...
+
+target_link_libraries(my-target QCoro::Core)
+```
+
+## QMake Usage
+
+```
+QT += QCoroCore
+```
+
+
+[qtdoc-qtcore]: https://doc.qt.io/qt-5/qtcore-index.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QFuture
+
+{{ doctable("Core", "QCoroFuture") }}
+
+[`QFuture`][qdoc-qfuture], which represents an asynchronously executed call, doesn't have any
+operation on its own that could be awaited asynchronously, this is usually done through a helper
+class called [`QFutureWatcher`][qdoc-qfuturewatcher]. To simplify the API, QCoro allows to directly
+`co_await` completion of the running `QFuture` or use of a wrapper class `QCoroFuture`. To wrap
+a `QFuture` into a `QCoroFuture`, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+template<typename T>
+QCoroFuture qCoro(const QFuture<T> &future);
+```
+
+It's possible to directly `co_await` a completion of a `QFuture` and obtain a result:
+
+```cpp
+const QString result = co_await QtConcurrent::run(...);
+```
+
+This is a convenient alternative to using `co_await qCoro(future).result()`.
+
+## `result()`
+
+Asynchronously waits for the `QFuture<T>` to finish and returns the result of the future. If the future
+result type is `void`, then returns nothing. If the asynchronous operation has thrown an exception, it
+will be rethrown here.
+
+Example:
+
+```cpp
+QFuture<QString> future = QtConcurrent::run(...);
+const QString result = co_await qCoro(future).result();
+```
+
+This is equivalent to using `QFutureWatcher` and then retrieving the result using
+[`QFuture::result()`][qdoc-qfuture-result]:
+
+```cpp
+QFuture<QString> future = QtConcurrent::run(...);
+auto watcher = new QFutureWatcher<QString>();
+connect(watcher, &QFutureWatcherBase::finished,
+ this, [watcher]() {
+ watcher->deleteLater();
+ auto result = watcher.result();
+ ...
+ });
+watcher->addFuture(future);
+```
+
+## `takeResult()`
+
+Asynchronously waits for the `QFuture<T>` to finish and returns the result of the future by taking
+(moving) it from the future object. If the asynchronous operation has thrown an exception, it will
+be rethrown here.
+
+This is useful when dealing with move-only types (like `std::unique_ptr`) or when you simply want to
+move the result instead of copying it out from the future.
+
+Example:
+
+```cpp
+QFuture<std::unique_ptr<Result>> future = QtConcurrent::run(...);
+auto result = co_await qCoro(future).takeResult();
+```
+
+This is equivalent to using `QFutureWatcher` and then retrieving the result using
+[`QFuture::takeResult()`][qdoc-qfuture-takeResult]:
+
+```cpp
+QFuture<std::unique_ptr<Result>> future = QtConcurrent::run(...);
+auto watcher = new QFutureWatcher<std::unique_ptr<Result>>();
+connect(watcher, &QFutureWatcherBase::finished,
+ this, [watcher]() {
+ watcher->deleteLater();
+ auto result = watcher.future().takeResult();
+ ...
+ });
+watcher->addFuture(future);
+```
+
+See documentation for [`QFuture::takeResult()`][qdoc-qfuture-takeResult] for limitations regarding
+moving the result out from `QFuture`.
+
+!!! info "This method is only available in Qt 6."
+
+## `waitForFinished()`
+
+This is equivalent to using the [`result()`](#result) method.
+
+## Example
+
+```cpp
+{% include "../../examples/qfuture.cpp" %}
+```
+
+[qdoc-qfuture]: https://doc.qt.io/qt-5/qfuture.html
+[qdoc-qfuturewatcher]: https://doc.qt.io/qt-5/qfuturewatcher.html
+[qdoc-qfuture-result]: https://doc.qt.io/qt-6/qfuture.html#result
+[qdoc-qfuture-takeResult]: https://doc.qt.io/qt-6/qfuture.html#takeResult
+[qcoro-coro]: ../coro/coro.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QIODevice
+
+{{
+ doctable("Core", "QCoroIODevice", None,
+ [('network/qabstractsocket', 'QCoroAbstractSocket'),
+ ('network/qlocalsocket', 'QCoroLocalSocket'),
+ ('network/qnetworkreply', 'QCoroNetworkReply'),
+ ('core/qprocess', 'QCoroProcess')])
+}}
+```cpp
+class QCoroIODevice
+```
+
+[`QIODevice`][qtdoc-qiodevice] has several different IO operations that can be waited on
+asynchronously. Since `QIODevice` itself doesn't provide the abaility to `co_await` those
+operations, QCoro provides a wrapper class called `QCoroIODevice`. To wrap a `QIODevice`
+into a `QCoroIODevice`, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroIODevice qCoro(QIODevice &);
+QCoroIODevice qCoro(QIODevice *);
+```
+
+Note that Qt provides several subclasses of `QIODevice`. QCoro provides coroutine-friendly
+wrappers for some of those types as well (e.g. for [`QLocalSocket`][qlocalsocket]). This
+subclass can be passed to `qCoro()` function as well. Oftentimes the wrapper class
+will provide some additional features (like co_awaiting establishing connection etc.).
+You can check whether QCoro supports the QIODevice subclass by checking the list of supported
+Qt types.
+
+## `readAll()`
+
+Waits until there are any data to be read from the device (similar to waiting until the device
+emits [`QIODevice::readyRead()`][qtdoc-qiodevice-readyread] signal) and then returns all data
+available in the buffer as a `QByteArray`. Doesn't suspend the coroutine if there are already
+data available in the `QIODevice` or if the `QIODevice` is not opened for reading.
+
+This is the default operation when `co_await`ing an instance of a `QIODevice` directly. Thus,
+it is possible to just do
+
+```cpp
+const QByteArray content = co_await device;
+```
+
+instead of
+
+```cpp
+const QByteArray content = co_await qCoro(device).readAll();
+```
+
+See documentation for [`QIODevice::readAll()`][qtdoc-qiodevice-readall] for details.
+
+`QCoroIODevice::readAll()` additionally accepts an optional timeout parameter. If no data
+become available within the timeout, the coroutine returns an empty `QByteArray`. If no
+timeout is specified or if it is set to `-1`, the operation will never time out.
+
+```cpp
+QCoro::Task<QByteArray> QCoroIODevice::readAll();
+QCoro::Task<QByteArray> QCoroIODevice::readAll(std::chrono::milliseconds timeout);
+```
+
+## `read()`
+
+Waits until there are any data to be read from the device (similar to waiting until the device
+emits [`QIODevice::readyRead()`][qtdoc-qiodevice-readyread] signal) and then returns up to
+`maxSize` bytes as a `QByteArray`. Doesn't suspend the coroutine if there are already data
+available in the `QIODevice` or if the device is not opened for reading.
+
+See documentation for [`QIODevice::read()`][qtdoc-qiodevice-read] for details.
+
+`QCoroIODevice::read()` additionally accepts an optional timeout parameter. If no data
+become available within the timeout, the coroutine returns an empty `QByteArray`. If no
+timeout is specified or if it is set to `-1`, the operation will never time out.
+
+```cpp
+QCoro::Task<QByteArray> QCoroIODevice::read(qint64 maxSize = 0);
+QCoro::Task<QByteArray> QCoroIODevice::read(qint64 maxSize, std::chrono::milliseconds timeout);
+```
+
+## `readLine()`
+
+Repeatedly waits for data to arrive until it encounters a newline character, end-of-data or
+until it reads `maxSize` bytes. Returns the resulting data as `QByteArray`.
+
+See documentation for [`QIODevice::readLine()`][qtdoc-qiodevice-readline] for details.
+
+`QCoroIODevice::readLine()` additionally accepts an optional timeout parameter. If no data
+become available within the timeout, the coroutine returns an empty `QByteArray`. If no
+timeout is specified or if it is set to `-1`, the operation will never time out.
+
+```cpp
+QCoro::Task<QByteArray> QCoroIODevice::readLine(qint64 maxSize = 0)
+QCoro::Task<QByteArray> QCoroIODevice::readLine(qint64 maxSize, std::chrono::milliseconds timeout);
+```
+
+## `waitForReadyRead()`
+
+Waits for at most `timeout_msecs` milliseconds for data to become available for reading
+in the `QIODevice`. Returns `true` when the device becomes ready for reading within the
+given timeout. Returns `false` if the operation times out, if the device is not opened
+for reading or in any other state in which the device will never become ready for reading.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QIODevice::waitForReadyRead()`][qtdoc-qiodevice-waitforreadyread] for details.
+
+```cpp
+QCoro::Task<bool> QCoroIODevice::waitForReadyRead(int timeout_msecs = 30'000);
+QCoro::Task<bool> QCoroIODevice::waitForReadyRead(std::chrono::milliseconds timeout);
+```
+
+## `waitForBytesWritten()`
+
+Waits for at most `timeout_msecs` milliseconds for data to be flushed from a buffered
+`QIODevice`. Returns `std::optional<qint64>`, which is empty if the operation has timed
+out, the device is not opened for writing or is in any other state in which the device
+will never be able to write any data. When the data are successfully flushed, returns
+number of bytes written.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QIODevice::waitForBytesWritten()`][qtdoc-qiodevice-waitforbyteswritten] for details.
+
+```cpp
+QCoro::Task<std::optional<qint64>> QCoroIODevice::waitForBytesWritten(int timeout_msecs = 30'000);
+QCoro::Task<std::optional<qint64>> QCoroIODevice::waitForBytesWritten(std::chrono::milliseconds timeout);
+```
+
+## Examples
+
+```cpp
+const QByteArray data = co_await qCoro(device).readAll();
+```
+
+[qlocalsocket]: ../network/qlocalsocket.md
+[qcoro-coro]: ../coro/coro.md
+[qtdoc-qiodevice]: https://doc.qt.io/qt-5/qiodevice.html
+[qtdoc-qiodevice-read]: https://doc.qt.io/qt-5/qiodevice.html#read
+[qtdoc-qiodevice-readyread]: https://doc.qt.io/qt-5/qiodevice.html#readyRead
+[qtdoc-qiodevice-readall]: https://doc.qt.io/qt-5/qiodevice.html#readAll
+[qtdoc-qiodevice-readline]: https://doc.qt.io/qt-5/qiodevice.html#readLine
+[qtdoc-qiodevice-waitforreadyread]: https://doc.qt.io/qt-5/qiodevice.html#waitForReadyRead
+[qtdoc-qiodevice-waitforbyteswritten]: https://doc.qt.io/qt-5/qiodevice.html#waitForBytesWritten
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QProcess
+
+{{ doctable("Core", "QCoroProcess", ("core/qiodevice", "QCoroIODevice")) }}
+
+[`QProcess`][qtdoc-qprocess] normally has two features to wait for asynchronously: the process to start
+and to finish. Since `QProcess` itself doesn't provide the ability to `co_await` those operations,
+QCoro provides a wrapper class `QCoroProcess`. To wrap a `QProcess` object into the `QCoroProcess`
+wrapper, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroProcess qCoro(QProcess &);
+QCoroProcess qCoro(QProcess *);
+```
+
+Same as `QProcess` is a subclass of `QIODevice`, `QCoroProcess` subclasses [`QCoroIODevice`][qcoro-qcoroiodevice],
+so it also provides the awaitable interface for selected QIODevice functions.
+See [`QCoroIODevice`][qcoro-qcoroiodevice] documentation for details.
+
+## `waitForStarted()`
+
+Waits for the process to be started or until it times out. Returns `bool` indicating
+whether the process has started successfuly (`true`) or timed out (`false`).
+
+See documentation for [`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted] for details.
+
+```cpp
+QCoro::Task<bool> QCoroProcess::waitForStarted(int timeout = 30'000);
+QCoro::Task<bool> QCoroProcess::waitForStarted(std::chrono::milliseconds timeout);
+```
+
+## `waitForFinished()`
+
+Waits for the process to finish or until it times out. Returns `bool` indicating
+whether the process has finished successfuly (`true`) or timed out (`false`).
+
+See documentation for [`QProcess::waitForFinished()`][qtdoc-qprocess-waitForFinished] for details.
+
+```cpp
+QCoro::Task<bool> QCoroProcess::waitForFinishedint timeout = 30'000);
+QCoro::Task<bool> QCoroProcess::waitForFinished(std::chrono::milliseconds timeout);
+
+```
+
+## `start()`
+
+QCoroProcess provides an additional method called `start()` which is equivalent to calling
+`QProcess::start()` followed by `QCoroProcess::waitForStarted()`. This operation is `co_awaitable`
+as well.
+
+See the documentation for [`QProcess::start()`][qtdoc-qprocess-start] and
+[`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted] for details.o
+
+Returns `true` when the process has successfully started, `false` otherwise.
+
+```cpp
+QCoro::Task<bool> QCoroProcess::start(QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+QCoro::Task<bool> QCoroProcess::start(const QString &program, const QStringList &arguments,
+ QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+```
+
+## Examples
+
+```cpp
+{% include "../../examples/qprocess.cpp" %}
+```
+
+
+[qtdoc-qprocess]: https://doc.qt.io/qt-5/qprocess.html
+[qtdoc-qprocess-start]: https://doc.qt.io/qt-5/qprocess.html#start
+[qtdoc-qprocess-waitForStarted]: https://doc.qt.io/qt-5/qprocess.html#waitForStarted
+[qtdoc-qprocess-waitForFiished]: https://doc.qt.io/qt-5/qprocess.html#waitForFinished
+[qcoro-coro]: ../coro/coro.md
+[qcoro-qcoroiodevice]: qiodevice.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QThread
+
+{{ doctable("Core", "QCoroThread") }}
+
+[`QThread`][qtdoc-qthread] has two events: `started` and `finished`. QCoro provides
+a coroutine-friendly wrapper for `QThread` - `QCoroThread`, which allows `co_await`ing
+those events.
+
+To wrap a `QThread` object into the `QCoroThread` wrapper, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroThread qCoro(QThread &);
+QCoroThread qCoro(QThread *);
+```
+
+## `waitForStarted()`
+
+Waits for the thread to start. Returns `true` if the thread is already running
+or when the thread starts within the specified timeout. If the thread has already
+finished or fails to start within the specified timeout, the coroutine will return
+`false`.
+
+If the timeout is set to -1, the operation will never time out.
+
+See documentation for [`QThread::started()`][qtdoc-qthread-started] for details.
+
+```cpp
+QCoro::Task<bool> QCoroThread::waitForStarted(std::chrono::milliseconds timeout);
+```
+
+## `waitForFinished()`
+
+Waits for the Waits for the process to finish or until it times out. Returns `bool` indicating
+whether the process has finished successfuly (`true`) or timed out (`false`).
+thread to finish. Returns `true` if the thread has already finished
+or if it finishes within the specified timeout. If the thread has not started yet
+or fails to stop within the specified timeout the coroutine will return `false`.
+
+If the timeout is set to -1, the operation will never time out.
+
+See documentation for [`QThread::finished()`][qtdoc-qthread-finished] for details.
+
+```cpp
+QCoro::Task<bool> QCoroThread::waitForFinished(std::chrono::milliseconds timeout);
+```
+
+## `QCoro::moveToThread()`
+
+A helper coroutine that allows changing the thread context in which the coroutine
+code is currently being executed.
+
+When `co_await`ed, the current coroutine will be suspended on the current thread and
+immediately resumed again, but in the context of the thread that was passed in as
+an argument.
+
+```cpp
+QCoro::Task<> QCoro::moveToThread(QThread *thread);
+```
+
+## Examples
+
+```cpp
+{% include "../../examples/qthread.cpp" %}
+```
+
+
+[qtdoc-qthread]: https://doc.qt.io/qt-5/qthread.html
+[qtdoc-qthread-started]: https://doc.qt.io/qt-5/qthread.html#started
+[qtdoc-qthread-finished]: https://doc.qt.io/qt-5/qthread.html#finished
+[qcoro-coro]: ../coro/coro.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QTimer
+
+{{ doctable("Core", "QCoroTimer") }}
+
+```cpp
+QTimer timer;
+timer.start(1s);
+co_await timer;
+```
+
+The QCoro frameworks allows `co_await`ing on [QTimer][qdoc-qtimer] object. The
+co-awaiting coroutine is suspended, until the timer finishes, that is until
+[`QTimer::timeout()`][qdoc-qtimer-timeout] signal is emitted.
+
+The timer must be active. If the timer is not active (not started yet or already
+finished) the `co_await` expression will return immediately.
+
+To make it work, include `QCoroTimer` in your implementation.
+
+```cpp
+#include <QCoroTimer>
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+QCoro::Task<> MyClass::pretendWork() {
+ // Creates and starts a QTimer that will tick every second
+ QTimer timer;
+ timer.setInterval(1s);
+ timer.start();
+
+ for (int i = 1; i <= 100; ++i) {
+ // Wait for the timer to tick
+ co_await timer;
+ // Update the progress bar value
+ mProgressBar->setValue(i);
+ // And repeat...
+ }
+ // ... until the for loop finishes.
+}
+```
+
+## `QCoro::sleepFor()`
+
+A simple coroutine that will suspend for the specified time duration. Can be quite
+useful especially in unit-tests.
+
+```cpp
+template<typename Rep, typename Period>
+QCoro::Task<> QCoro::sleepFor(const std::chrono::duration<Rep, Period> &timeout);
+```
+
+## `QCoro::sleepUntil()`
+
+A simple coroutine that will suspend until the specified point in time. Can be useful
+for scheduling tasks.
+
+```cpp
+template<typename Clock, typename Duration>
+QCoro::Task< QCoro::sleepUntil(const std::chrono::time_point<Clock, Duration> &when);
+```
+
+Example:
+
+```cpp
+const auto now = std::chrono::system_clock::now();
+const auto tomorrow_time = std::chrono::system_clock::to_time_t(now + 86400s);
+std::tm *gt = std::gmtime(&tomorrow_time);
+gt.tm_hour = 0;
+gt.tm_min = 0;
+gt.tm_sec = 0;
+const auto tomorrow_midnight = std::mktime(>);
+co_await QCoro::sleepUntil(std::chrono::system_clock::from_time_t(tomorrow_midnight));
+```
+
+[qdoc-qtimer]: https://doc.qt.io/qt-5/qtimer.html
+[qdoc-qtimer-timeout]: https://doc.qt.io/qt-5/qtimer.html#timeout
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+{{ doctable("Core", "QCoroSignal") }}
+
+# Signals
+
+It's possible to `co_await` a single signal emission through a special
+overload of the [`qCoro()`][qcoro-coro] function. The below function returns
+an awaitable that will suspend the current coroutine until the specified signal
+is emitted.
+
+```cpp
+Task<SignalResult> qCoro(QObject *obj, QtSignalPtr ptr);
+```
+
+The arguments are a pointer to a QObject-derived object and a pointer
+to a the object's signal to connect to. Note that if the object is destroyed
+while being `co_await`ed, the coroutine will never be resumed.
+
+The returned awaitable produces the signal's arguments. That is, if the
+signal has no arguments, the result of the awaitable will be `void`. If
+the signal has exactly one argument, then the awaitable produces the value
+of the argument. If the signal has more than one arguments, then the result
+of the awaitable is a `std::tuple` with all the arguments.
+
+Example:
+```
+// void signal
+co_await qCoro(timer, &QTimer::timeout);
+
+// single-argument signal
+const QUrl url = co_await qCoro(reply, &QNetworkReply::redirected);
+
+// multi-argument signal, captured using structured bindings
+const auto [exitCode, exitStatus] = co_await qCoro(process, &QProcess::finished);
+```
+
+```cpp
+Task<std::optional<SignalResult>> qCoro(QObject *obj, QtSignalPtr ptr, std::chrono::milliseconds timeout);
+```
+
+An overload that behaves similar to the two-argument overload, but takes an additional
+`timeout` argument. If the signal is not emitted within the specified timeout, the returned
+awaitable produces an empty `std::optional`. Otherwise the return type behaves the same way
+as the two-argument overload.
+
+## QCoroSignalListener
+
+A helper function that creates an [`AsyncGenerator`][qcoro-asyncgenerator] which yields a value
+whenever the signal is emitted.
+
+```cpp
+QCoro::AsyncGenerator<SignalArgs> qCoroSignalListener(QObject *obj, QtSignalPtr ptr, std::chrono::milliseconds timeout);
+```
+
+The function takes up to three arguments, the `obj` and `ptr` are a QObject-derived object and
+a pointer to a signal member function to connect to. The third `timeout` argument is optional.
+When the timeout is set, the generator will end if the signal is not emitted within the specified
+timeout. When not set, or set to -1, the generator will never terminate on its own, even if
+the `obj` QObject is destroyed!
+
+The generator produces all signal emissions, even those that ocur in between the generator being
+`co_await`ed. In the example below, even when the `QNetworkReply::downloadProgress()` signal is
+emitted while asynchronously processing something in the middle of the `while` loop body, the
+emission will not be lost. It will be enqueued, and returned synchronously with the next
+`co_await ++it` call.
+
+```cpp
+auto listener = qCoroSignalListener(networkReply, &QNetworkReply::downloadProgress);
+auto it = co_await listener.begin(); // waits for first emission
+while (it != listener.end() && networkReply->isRunning()) {
+ const auto [received, total] = *it;
+ // do something with results
+ //...
+ if (received == total || networkReply->isFinished()) {
+ break;
+ }
+ co_await ++it; // waits for next signal emission
+}
+```
+
+Alternatively, it's possible to use `QCORO_FOREACH` to look over the generator:
+
+```cpp
+QCORO_FOREACH(const auto [received, total], qCoroSignalListener(reply, &QNetworkReply::downloadProgress)) {
+ // do something with `received` and `total` values
+ if (received == total || reply->isFinished()) {
+ break;
+ }
+}
+```
+
+!!! warning "Listener doesn't stop listening until destroyed."
+ Keep in mind that the `listener` generator will keep listening and collecting the
+ signal emissions until it is destroyed, even if you are no longer actively iterating
+ over the generator. It is recommended that you destroy the generator as soon as possible
+ when you no longer need it.
+
+
+[qcoro-coro]: ../coro/coro.md
+[qcoro-asyncgenerator]: ../coro/asyncgenerator.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro::AsyncGenerator<T>
+
+{{ doctable("Coro", "QCoroAsyncGenerator") }}
+
+```cpp
+template<typename T> class QCoro::AsyncGenerator
+```
+
+`AsyncGenerator<T>` is fundamentally identical to [`QCoro::Generator<T>`][qcoro-generator].
+The only difference is that the value is produced asynchronously. Asynchronous
+generator is used exactly the same way as synchronous generators, except that the
+result of `AsyncGenerator<T>::begin()` must be `co_await`ed, and incrementing
+the returned iterator must be `co_await`ed as well.
+
+```cpp
+QCoro::AsyncGenerator<uint8_t> lottoNumbersGenerator(int count) {
+ Hat hat; // Hat with numbers
+ Hand hand; // Hand to pull numbers out of the hat
+ for (int i = 0; i < count; ++i) {
+ const uint8_t number = co_await hand.pullNumberFrom(hat);
+ co_yield number; // guaranteed to be a winning number
+ }
+}
+
+QCoro::Task<> winningLottoNumbers() {
+ const auto makeMeRich = lottoNumbersGenerator(10);
+ // We must co_await begin() to obtain the initial iterator
+ auto winningNumber = co_await makeMeRich.begin();
+ std::cout << "Winning numbers: ";
+ while (winningNumber != makeMeRich.end()) {
+ std::cout << *winningNumber;
+ // And we must co_await increment
+ co_await ++(winningNumber);
+ }
+}
+```
+
+You might be wondering why are we `co_await`ing `AsyncGenerator<T>::begin()` and
+`AsyncGeneratorIterator<T>::operator++()`, and not just the value (the result of
+dereferencing the iterator) - it would surely make the code simpler and more intuitive.
+The simple reason is that we are `co_await`ing the next iterator (which either holds
+a value or is past-the-end iterator) rather than the value itself. Once we have the
+iterator, we must check whether it's valid or not, because paste-the-end iterator is
+not dereferencable. That's why the `AsyncGenerator<T>::begin()` and
+`AsyncGeneratorIterator<T>::operator++()` operations are asynchronous, rather than
+`AsyncGeneratorIterator<T>::operator*()`.
+
+#### Exceptions
+
+When the generator coroutine throws an exception, the exception will be rethrown from
+the `operator++()` (or from generator's `begin()`) function when they are `co_await`ed.
+
+Afterwards, the iterator is considered invalid and the generator is finished and may not
+be used anymore.
+
+```cpp
+QCoro::AsyncGenerator<int> generatorThatThrows() {
+ while (true) {
+ const int val = co_await generateRandomNumber();
+ if (val == 0) {
+ throw std::runtime("Division by zero!");
+ }
+ co_yield 42 / val;
+ }
+}
+
+QCoro::Task<> divide42ForNoReason(std::size_t count) {
+ auto generator = generatorThatThrows();
+ std::vector<int> numbers;
+ try {
+ // Might throw if generator generates 0 immediately.
+ auto it = co_await generator.begin();
+ while (numbers.size() < count) {
+ numbers.push_back(*it);
+ co_await ++it; // might throw
+ }
+ } catch (const std::runtime_error &e) {
+ // We were unable to generate all numbers
+ }
+}
+```
+
+### QCORO_FOREACH
+
+```cpp
+#define QCORO_FOREACH(value, generator)
+```
+
+The example in previous chapter shows one example of looping over values produced
+by an asynchronous generator with a `while` loop. This is how it would look like
+with a `for` loop:
+
+```cpp
+auto generator = createGenerator();
+for (auto it = co_await generator.begin(), end = generator.end(); it != end; co_await ++it) {
+ const QString &value = *it;
+ ...//do something with value
+}
+```
+
+Sadly, it's not possible to use the generator with a ranged-based for loop. While
+initially the proposal for C++ coroutines did contain `for co_await` construct, it
+was removed as the committee was [concerned that it was making assumptions about the
+future interface of asynchronous generators][p0664r8c35].
+
+For that reason, QCoro provides `QCORO_FOREACH` macro, which is very similar to
+[`Q_FOREACH`][qdoc-qforeach] and works exactly like the for-loop in the example
+above:
+
+```cpp
+QCORO_FOREACH(const QString &value, createGenerator()) {
+ ... // do something with value
+}
+```
+
+[qcoro-generator]: ./generator.md
+[p0664r8c35]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0664r8.html#35
+[qdoc-qforeach]: https://doc.qt.io/qt-5/qtglobal.html#Q_FOREACH
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# qCoro()
+
+## Wrapping Qt Types
+
+```cpp
+QCoroType qCoro(QtClass *);
+QCoroType qCoro(QtClass &);
+```
+
+This function is overloaded for all Qt types supported by this library. It accepts either
+a pointer or a reference to a Qt type, and returns a QCoro type that wraps the Qt type and
+provides coroutine-friendly API for the type.
+
+Some objects have only a single asynchronous event, so it makes sense to make them
+directly `co_await`able. An example is `QTimer`, where only one thing can be `co_await`ed -
+the timer timeout. Thus with QCoro, it's possible to simply do this:
+
+```cpp
+QTimer timer;
+...
+co_await timer;
+```
+
+However, some Qt classes have multiple asynchronous operations that the user may want to `co_await`.
+For such types, simply `co_await`ing the class instance doesn't make sense since it's not clear
+what operation is being `co_await`ed. For those types, QCoro provides `qCoro()` function
+which returns a wrapper that provides coroutine-friendly versions of the asynchronous methods
+for the given type.
+
+Let's take QProcess as an example: one may want to `co_await` for the program to start or finish.
+Therefore the type must be wrapped into `qCoro()` like this:
+
+```cpp
+QProcess process;
+// Wait for the process to be started
+co_await qCoro(process).start(...);
+// The process is running now
+...
+...
+// Wait for it to finish
+co_await qCoro(process).finished();
+// The process is no longer running
+...
+```
+
+`qCoro()` is simply overloaded for all the Qt types currently supported by the QCoro library.
+The function returns a wrapper object (e.g. `QCoro::detail::QCoroProcess`) which copies the
+QProcess API. It doesn't copy the entire API, only the bits that we want to make `co_await`able.
+When you call one of those metods (e.g. `QCoroProcess::start()`), it returns an awaitable
+type that calls `QProcess::start()`, suspends the coroutine and resumes it again when the
+wrapped `QProcess` object emits the `started()` signal.
+
+Normally you don't need to concern yourself with anything inside the `QCoro::detail` namespace,
+it's mentioned in the previous paragraph simply to explain how the wrapper works.
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro::Generator<T>
+
+{{ doctable("Coro", "QCoroGenerator") }}
+
+```cpp
+template<typename T> class QCoro::Generator
+```
+
+Generator is a coroutine that that yields a single value and then
+suspends itself, until resumed again. Then it yields another value
+and suspends again. Generator can be inifinite (the coroutine will
+never finish and will produce new values for ever until destroyed
+from the outside) or finite (after generating some amount of values
+the coroutine finishes).
+
+The `QCoro::Generator<T>` is a template class providing interface
+for the generator consumer. There is no standard API for generators
+specified in the C++ standard (as of C++20). The design chosen by
+QCoro copies the design of cppcoro library, which is for the `Generator<T>`
+class to provide `begin()` and `end()` methods to obtain iterator-
+like objects, allowing the generators to be used like containers and
+providing an interface that is familiar to every C++ programmer.
+
+```cpp
+// Simple generator that produces `count` values from 0 to `(count-1)`.
+QCoro::Generator<int> iota(int count) {
+ for (int i = 0; i < count; ++i) {
+ // Yields a value and suspends the generator.
+ co_yield count;
+ }
+}
+
+void counter() {
+ // Creates a new initially suspended generator coroutine
+ auto generator = iota(10);
+ // Resumes the coroutine, obtains first value and returns
+ // an iterator representing the first value.
+ auto it = generator.begin();
+ // Obtains a past-the-end iterator.
+ const auto end = generator.end();
+
+ // Loops until the generator doesn't finish.
+ while (it != end) {
+ // Resumes the generator until it co_yields another value.
+ ++it;
+ // Reads the current value from the iterator.
+ std::cout << *it << std::endl;
+ }
+
+ // The code above can be written more consisely using ranged based for-loop
+ for (const auto value : iota(10)) {
+ std::count << value << std::endl;
+ }
+}
+```
+
+## Infinite generators
+
+A generator may be inifinite, that is it may never finish and keep
+producing values whenever queried. A simple naive example might be
+a generator producing random value whenever resumed.
+
+See the next chapter on generator lifetime regarding how to destroy
+an infinite generator.
+
+
+```cpp
+QCoro::Generator<quint32> randomNumberGenerator() {
+ auto *generator = QRandomGenerator::system();
+ while (true) {
+ // Generates a single random value and suspends.
+ co_yield generator->generate();
+ }
+}
+
+void randomInitialize(QVector<quint32> &vector) {
+ // Constructs the generator
+ auto rng = randomNumberGenerator();
+ // Gets the first tandom value
+ auto rngIt = rng.begin();
+ // Loops over all values of the vector
+ for (auto &val : vector) {
+ // Stores the current random value and generates a next one
+ val = *(rngIt++);
+ }
+} // Destroyes the generator coroutine.
+```
+
+## Generator lifetime
+
+The lifetime of the generator coroutine is tight to the lifetime
+of the associated `QCoro::Generator<T>` object. The generator
+coroutine is destroyed when the associated `QCoro::Generator<T>`
+object is destroyed, that includes the stack of the coroutine
+and everything allocated on the stack.
+
+It doesn't matter whether the coroutine has already finished or
+whether it is suspended after yielding a value. When the
+`QCoro::Generator<T>` object is destroyed, the stack of the
+coroutine and all associated state will be destroyed using the
+regular rules of stack destruction.
+
+Therefore, it is stafe to allocate values on generator coroutine
+stack. However, dynamically allocated memory will not be free'd
+automatically. Therefore if you need to dynamically allocate
+memory on heap inside the generator coroutine, you must make
+sure to either free it before the generator coroutine is suspended,
+or that it is destroyed when the stack is destroyed, e.g. by using
+it in `std::unique_ptr` or `QScopeGuard`.
+
+!!! info "Memory usage"
+ Keep in mind, that that generator coroutine will keep occupying
+ memory even when not used until it finishes or until the
+ associated `QCoro::Generator<T>` is destroyed.
+
+## Exceptions
+
+When a generator coroutine throws an exception, it will be rethrown
+from `operator++()` or from generator's `begin()` method.
+
+Afterwards the iterator is considered invalid and the generator
+is finished and may not be used anymore.
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Coro module
+
+The Coro module contains the fundamental coroutine types - the
+[QCoro::Task<T>][qcoro-task] for eager coroutines,
+[QCoro::LazyTask<T>][qcoro-lazytask] for lazy coroutines,
+[QCoro::Generator<T>][qcoro-generator] for synchronous generators and
+[QCoro::AsyncGenerator<T>][qcoro-asyncgenerator] for asynchronous generators.
+Another useful bit of the Coro module is the [qCoro()][qcoro-coro] wrapper
+function that wraps native Qt types into a coroutine-friendly versions supported by
+QCoro (check the [Core][qcoro-core], [Network][qcoro-network] and
+[DBus][qcoro-dbus] modules of QCoro to see which
+Qt types are currently supported by QCoro).
+
+If you don't want to use any of the Qt types supported by QCoro in your
+code, but you still want to use C++ coroutines with QCoro, you can simply
+just link against `QCoro::Coro` target in your CMakeLists.txt. This will
+give you all you need to start implementing custom coroutine-native types
+with Qt and QCoro.
+
+[qcoro-task]: task.md
+[qcoro-lazytask]: lazytask.md
+[qcoro-coro]: coro.md
+[qcoro-generator]: generator.md
+[qcoro-asyncgenerator]: asyncgenerator.md
+[qcoro-core]: ../core/index.md
+[qcoro-network]: ../network/index.md
+[qcoro-dbus]: ../dbus/index.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro::LazyTask
+
+{{ doctable("Coro", "QCoroLazyTask", None, [], "0.11") }}
+
+```cpp
+template<typename T> class QCoro::LazyTask
+```
+
+`QCoro::LazyTask<T>` represents, as the name suggests, a lazily evaluated coroutine. A lazily
+evaluated coroutine is suspended immediately when called and will not execute its body until
+the returned `QCoro::LazyTask<T>` is `co_await`ed.
+
+This is in contrast to `QCoro::Task<T>`, which is an eager coroutine, meaning that its body
+is executed immediately when invoked and is only suspended when it first `co_await`s another
+coroutine.
+
+!!! warning "Don't use LazyTask<T> as a Qt slot"
+
+ Do not use `LazyTask<T>` as a return type of Qt slots, or any other coroutine that can be
+ invoked by the Qt event loop. The Qt event loop is not aware of coroutines and will never
+ `co_await` the returned `LazyTask<T>`. Therefore, the coroutine's body would never be
+ executed!
+
+ This is the main reason why the "default" `QCoro::Task<T>` coroutines are eager - they
+ don't need to be `co_await`ed in order to be executed, which makes them compatible with
+ the Qt event loop.
+
+ If you need to have a lazy coroutine that is also invokable from the Qt event loop, use an
+ eager wrapper coroutine to pass to the event loop:
+
+ ```cpp
+ QCoro::LazyTask<> myLazyCoroutine();
+
+ Q_INVOKABLE QCoro::Task<> myLazyCoroutineWrapper() {
+ co_await myLazyCoroutine();
+ }
+ ```
+
+ The eager wrapper is always immediately executed, and since it will immediately start
+ `co_await`ing on the lazy coroutine, the lazy coroutine will effectively get executed
+ immediately.
+
+## `then()` continuation
+
+It is possible to chain a continuation to the coroutine by using `.then()`. It is possible to
+use both lazy and eager continuations and even non-coroutine continuations:
+
+```cpp
+auto task = myLazyTask().then([]() -> QCoro::Task<> { ... }); // #1
+auto task = myLazyTask().then([]() -> QCoro::LazyTask<> { ... }); // #2
+auto task = myLazyTask().then([]() { return 42; }) // #3
+```
+
+In case #1, the `myLazyTask()` will be evaluated eagerly becaues of the `.then()` continuation being
+eager (basically equivalent to the eager wrapper mentioned in the warning note at the top).
+
+In case #2 and #3, the the entire chain will be evaluated lazily - that is, both the `myLazyTask()` and the
+`.then()` continuation will be evaluated only once `task` is `co_await`ed.
+
+## Blocking wait
+
+It's possible to use `QCoro::waitFor()` to synchronously wait for completion of a lazy coroutine.
+Check the documentation in [`QCoro::Task<T>`](task.md#Blocking-wait) for details.
+
+## Interfacing with synchronous functions
+
+See [`QCoro::connect() documentation for `QCoro::Task<T>`](task.md#interfacing-with-synchronous-functions)
+for details.
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QCoro::Task
+
+{{ doctable("Coro", "QCoroTask") }}
+
+```cpp
+template<typename T> class QCoro::Task
+```
+
+Any coroutine that wants to `co_await` one of the types supported by the QCoro library must have
+return type `QCoro::Task<T>`, where `T` is the type of the "regular" coroutine return value.
+
+There's no need by the user to interact with or construct `QCoro::Task` manually, the object is
+constructed automatically by the compiler before the user code is executed. To return a value
+from a coroutine, use `co_return`, which will store the result in the `Task` object and leave
+the coroutine.
+
+```cpp
+QCoro::Task<QString> getUserName(UserID userId) {
+ ...
+
+ // Obtain a QString by co_awaiting another coroutine
+ const QString result = co_await fetchUserNameFromDb(userId);
+
+ ...
+
+ // Return the QString from the coroutine as you would from a regular function,
+ // just use `co_return` instead of `return` keyword.
+ co_return result;
+}
+```
+
+To obtain the result of a coroutine that returns `QCoro::Task<T>`, the result must be `co_await`ed.
+When the coroutine `co_return`s a result, the result is stored in the `Task` object and the `co_await`ing
+coroutine is resumed. The result is obtained from the returned `Task` object and returned as a result
+of the `co_await` call.
+
+```cpp
+QCoro::Task<void> getUserDetails(UserID userId) {
+ ...
+
+ const QString name = co_await getUserName(userId);
+
+ ...
+}
+```
+
+!!! info "Exception Propagation"
+ When coroutines throws an unhandled exception, the exception is stored in the `Task` object and
+ is re-thrown from the `co_await` call in the awaiting coroutine.
+
+Note that a default-constructed `Task<T>` object is not associated with any coroutine and awaiting
+on it will suspend the awaiter indefinitely. Moving into a default constructed `Task<T>` associates
+it with the coroutine previously associated with the moved-from `Task<T>`, and so awaiting on the
+moved-to `Task<T>` will work as expcted. On the other hand, the moved-from `Task<T>` will no longer
+be associated with any coroutine and awaiting on it will behave the same as awaiting a default-
+constructed task - it will suspend the awaiter indefinitely.
+
+
+## `then()` continuation
+
+!!! note "This feature is available since QCoro 0.5.0"
+
+Sometimes it's not possible to `co_await` a coroutine, for example when calling a coroutine from a
+reimplementation of a virtual function from a 3rd party library, where we cannot change the signature
+of that function to be a coroutine (e.g. a reimplementation of `QAbstractItemModel::data()`).
+
+Even in this case, we want to process the result of the coroutine asynchronously, though. For such
+cases, `Task<T>` provides a `then()` member function that allows the caller to provide a custom
+continuation callback to be invoked when the coroutine finishes.
+
+---
+
+```cpp
+template<typename ThenCallback>
+requires (std::invocable<ThenCallback> || (!std::is_void<T> && std::invocable<ThenCallback, T>))
+Task<R> Task<T>::then(ThenCallback callback);
+```
+
+The `Task<T>::then()` member function has two arguments. The first argument is the continuation
+that is called when the coroutine finishes. The second argument is optional - it is a callable
+that gets invoked instead of the continuation when the coroutine throws an exception.
+
+The continuation callback must be a callable that accepts either zero arguments (effectively
+discardin the result of the coroutine), or exactly one argument of type `T` or type implicitly
+constructible from `T`.
+
+If the return type of the `ThenCallback` is `void`, then the return type of the `then()` functon is
+`Task<void>`. If the return type of the `ThenCallback` is `R` or `Task<R>`, the return type of the
+`then()` function is `Task<R>`. This means that the `ThenCallback` can be a coroutine as well. Thanks
+to the return type always being of type `Task<R>`, it is possible to chain multiple `.then()` calls,
+or `co_await` the result of the entire chain.
+
+If the coroutine throws an exception, the exception is re-thrown when the result of the entire
+continuation is `co_await`ed. If the result of the continuation is not `co_await`ed, the exception
+is silently ignored.
+
+If an exception is thrown from the `ThenCallback`, then the exception is either propagated to the nex
+chained `then()` continuation or re-thrown if directly `co_await`ed. If the result is not `co_await`ed
+and no futher `then()` continuation is chained after the one that has thrown, then the exception is
+silently ignored.
+
+---
+
+```cpp
+template<typename ThenCallback, typename ErrorCallback>
+requires (((std::is_void_t<T> && std::invocable<ThenCallback>) || std::invocable<ThenCallback, T>)
+ && std::invocable<ErrorCallback, const std::exception &>)
+Task<R> Task<T>::then(ThenCallback thenCallback, ErrorCallback errorCallback);
+```
+
+An overload of the `then()` member function which takes an additional callback to be invoked when
+an exception is thrown from the coroutine. The `ErrorCallback` must be a callable that takes exactly
+one argument, which is `const std::exception &`, holding reference to the exception thrown. An exception
+thrown from the `ErrorCallback` will be re-thrown if the entire continuation is `co_await`ed. If another
+`.then()` continuation is chained after the current continuation and has an `ErrorCallback`, then the
+`ErrorCallback` will be invoked. Otherwise, the exception is silently ignored.
+
+If an exception is thrown by the non-void coroutine and is handled by the `ErrorCallback`, then if the
+resulting continuation is `co_await`ed, the result will be a default-constructed instance of type `R`
+(since the `ThenCallback` was unable to provide a proper instance of type `R`). If `R` is not default-
+constructible, the program will not compile. Thus, if returning a non-default-constructible type from
+a coroutine that may throw an exception, we recommend to wrap the type in `std::optional`.
+
+Examples:
+
+```cpp
+QString User::name() {
+ if (mName.isNull()) {
+ mApi.fetchUserName().then(
+ [this](const QString &name) {
+ mName = name;
+ Q_EMIT nameChanged();
+ }, [](const std::exception &e) {
+ mName = QStringLiteral("Failed to fetch name: %1").arg(e.what());
+ Q_EMIT nameChanged();
+ });
+ return QStringLiteral("Loading...");
+ } else {
+ return mName;
+ }
+}
+```
+
+## Blocking wait
+
+Sometimes it's necessary to wait for a coroutine in a blocking manner - this is especially useful
+in tests where possibly no event loop is running. QCoro has `QCoro::waitFor()` function
+which takes `QCoro::Task<T>` (that is, result of calling any QCoro-based coroutine) and blocks
+until the coroutine finishes. If the coroutine has a non-void return value, the value is returned
+from `waitFor().`
+
+Since QCoro 0.8.0 it is possible to use `QCoro::waitFor()` with any awaitable type, not just `QCoro::Task<T>`.
+
+```cpp
+QCoro::Task<int> computeAnswer() {
+ co_await QCoro::sleepFor(std::chrono::years{7'500'00});
+ co_return 42;
+}
+
+void nonCoroutineFunction() {
+ // The following line will block as if computeAnswer were not a coroutine. It will internally run a
+ // a QEventLoop, so other events are still processed.
+ const int answer = QCoro::waitFor(computeAnswer());
+ std::cout << "The answer is: " << answer << std::endl;
+}
+```
+
+!!! info "Event loops"
+ The implementation internally uses a `QEventLoop` to wait for the coroutine to be completed.
+ This means that a `QCoreApplication` instance must exist, although it does not need to be
+ executed. Usual warnings about using a nested event loop apply here as well.
+
+## Interfacing with synchronous functions
+
+!!! note "This feature is available since QCoro 0.7.0"
+
+Sometimes you need to interface with code that is not coroutine-aware, for example when building list models.
+
+If you'd use the normal `.then()` function, and the coroutine would finish after the model is deleted, the program would crash.
+
+The `QCoro::connect` function is similar to `QObject::connect`,
+but for `QCoro::Task`s.
+Just like `QObject::connect`, it only calls its callback if the context object still exists.
+
+This is an example for a function that fetches data and updates a model:
+```cpp
+void updateModel() {
+ auto task = someCoroutine();
+ QCoro::connect(std::move(task), this, [this](auto &&result) {
+ beginResetModel();
+ m_entries = std::move(result);
+ endResetModel();
+ });
+}
+```
+
+If the model is deleted before the coroutine finishes, the connected lambda will not be called.
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# DBus Module
+
+The `DBus` module contains coroutine-friendly wrapper for
+[QtDBus][qtdoc-qtdbus] classes.
+
+## CMake Usage
+
+```
+find_package(QCoro6 COMPONENTS DBus)
+
+...
+
+target_link_libraries(my-target QCoro::DBus)
+```
+
+## QMake Usage
+
+```
+QT += QCoroDBus
+```
+
+[qtdoc-qtdbus]: https://doc.qt.io/qt-5/qtdbus-index.html
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QDBusPendingCall
+
+{{ doctable("DBus", "QCoroDBusPendingCall") }}
+
+
+[`QDBusPendingCall`][qdoc-qdbuspendingcall] on its own doesn't have any operation that could
+be awaited asynchronously, this is usually done through a helper class called
+[`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher]. To simplify the API, QCoro allows to
+directly `co_await` completion of the pending call or use a wrapper class `QCoroDBusPendingCall`.
+To wrap a `QDBusPendingCall` into a `QCoroDBusPendingCall`, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroDBusPendingCall qCoro(const QDBusPendingCall &);
+```
+
+To await completion of the pending call without the `qCoro` wrapper, just use the pending call
+in a `co_await` expression. The behavior is identical to awaiting on result of
+`QCoroDBusPendingCall::waitForFinished()`.
+
+```cpp
+QDBusPendingCall call = interface.asyncCall(...);
+const QDBusReply<...> reply = co_await pendigCall;
+```
+
+!!! info "`QDBusPendingCall` vs. `QDBusPendingReply`"
+ As the Qt documentation for [`QDBusPendingCall`][qdoc-qdbuspendingcall] says, you are more likely to use
+ [`QDBusPendingReply`][qdoc-qdbuspendingreply] in application code than `QDBusPendingCall`. QCoro has explicit
+ support for `QDBusPendingCall` to allow using functions that return `QDBusPendingCall` directly in `co_await`
+ expressions without the programmer having to first convert it to `QDBusPendingReply`. `QDBusPendingReply` can
+ be constructed from a `QDBusMessage`, which is a result of awaiting `QDBusPendingCall`, therefore it's possible
+ to perform both the conversion and awaiting in a single line of code:
+ ```cpp
+ QDBusPendingReply<...> reply = co_await iface.asyncCall(...);
+ ```
+ Note that [`QDBusAbstractInterface::asyncCall`][qdoc-qdbusabstractinterface-asyncCall] returns
+ a `QDBusPendingCall`.
+
+## `waitForFinished()`
+
+{% include-markdown "../../../qcoro/dbus/qcorodbuspendingcall.h"
+ dedent=true
+ rewrite-relative-urls=false
+ start="<!-- doc-waitForFinished-start -->"
+ end="<!-- doc-waitForFinished-end -->" %}
+
+## Example
+
+```cpp
+{% include "../../examples/qdbus.cpp" %}
+```
+
+[qdoc-qdbuspendingcall]: https://doc.qt.io/qt-5/qdbuspendingcall.html
+[qdoc-qdbuspendingreply]: https://doc.qt.io/qt-5/qdbuspendingreply.html
+[qdoc-qdbuspendingcallwatcher]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html
+[qdoc-qdbuspendingcallwatcher-finished]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html#finished
+[qdoc-qdbusabstractinterface-asyncCall]: https://doc.qt.io/qt-5/qdbusabstractinterface.html#asyncCall-1
+[qcoro-coro]: ../coro/coro.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QDBusPendingReply
+
+ {{ doctable("DBus", "QCoroDBusPendingReply") }}
+
+[`QDBusPendingReply`][qdoc-qdbuspendingreply] on its own doesn't have any operation that could
+be awaited asynchronously, this is usually done through a helper class called
+[`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher]. To simplify the API, QCoro allows to
+directly `co_await` completion of the pending reply or use a wrapper class `QCoroDBusPendingReply`.
+To wrap a `QDBusPendingReply` into a `QCoroDBusPendingReply`, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+template<typename ... Args>
+QCoroDBusPendingCall qCoro(const QDBusPendingReply<Args ...> &);
+```
+
+!!! info "`QDBusPendingReply` in Qt5 vs Qt6"
+ `QDBusPendingReply` in Qt6 is a variadic template, meaning that it can take any amount of template arguments.
+ In Qt5, however, `QDBusPendingReply` is a template class that accepts only up to 8 paremeters. In QCoro the
+ `QCoroDBusPendingReply` wrapper is implemented as a variadic template for compatibility with Qt6, but when
+ building against Qt5, the number of template parameters is artificially limited to 8 to mirror the limitation
+ of Qt5 `QDBusPendingReply` limitation.
+
+To await completion of the pending call without the `qCoro` wrapper, just use the pending call
+in a `co_await` expression. The behavior is identical to awaiting on result of
+`QCoroDBusPendingReply::waitForFinished()`.
+
+```cpp
+QDBusPendingReply<...> reply = interface.asyncCall(...);
+co_await reply;
+// Now the reply is finished and the result can be retrieved.
+```
+
+## `waitForFinished()`
+
+{% include-markdown "../../../qcoro/dbus/qcorodbuspendingreply.h"
+ dedent=true
+ rewrite-relative-urls=false
+ start="<!-- doc-waitForFinished-start -->"
+ end="<!-- doc-waitForFinished-end -->" %}
+
+## Example
+
+```cpp
+{% include "../../examples/qdbus.cpp" %}
+```
+
+[qdoc-qdbuspendingcall]: https://doc.qt.io/qt-5/qdbuspendingcall.html
+[qdoc-qdbuspendingreply]: https://doc.qt.io/qt-5/qdbuspendingreply.html
+[qdoc-qdbuspendingcallwatcher]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html
+[qdoc-qdbuspendingcallwatcher-finished]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html#finished
+[qdoc-qdbusabstractinterface-asyncCall]: https://doc.qt.io/qt-5/qdbusabstractinterface.html#asyncCall-1
+[qcoro-coro]: ../coro/coro.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Network Module
+
+The `Network` module contains coroutine-friendly wrapper for
+[QtNetwork][qtdoc-qtnetwork] classes.
+
+## CMake Usage
+
+```
+find_package(QCoro6 COMPONENTS Network)
+
+...
+
+target_link_libraries(my-target QCoro::Network)
+```
+
+## QMake Usage
+
+```
+QT += QCoroNetwork
+```
+
+[qtdoc-qtnetwork]: https://doc.qt.io/qt-5/qtnetwork-index.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QAbstractSocket
+
+{{ doctable("Network", "QCoroAbstractSocket", ("core/qiodevice", "QCoroIODevice")) }}
+
+[`QAbstractSocket`][qtdoc-qabstractsocket] is a base class for [`QTcpSocket`][qtdoc-qtcpsocket]
+and [`QUdpSocket`][qtdoc-qudpsocket] and has some potentially asynchronous operations.
+In addition to reading and writing, which are provided by [`QIODevice`][qtdoc-qiodevice]
+baseclass and can be used with coroutines thanks to QCoro's [`QCoroIODevice`][qcoro-qcoroiodevice]
+it provides asynchronous waiting for connecting to and disconnecting from the server.
+
+Since `QAbstractSocket` doesn't provide the ability to `co_await` those operations, QCoro provides
+ a wrapper calss `QCoroAbstractSocket`. To wrap a `QAbstractSocket` object into the `QCoroAbstractSocket`
+ wrapper, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroAbstractSocket qCoro(QAbstractSocket &);
+QCoroAbstractSocket qCoro(QAbstractSocket *);
+```
+
+Same as `QAbstractSocket` is a subclass of `QIODevice`, `QCoroAbstractSocket` subclasses
+[`QCoroIODevice`][qcoro-qcoroiodevice], so it also provides the awaitable interface for selected
+`QIODevice` functions. See [`QCoroIODevice`][qcoro-qcoroiodevice] documentation for details.
+
+## `waitForConnected()`
+
+Waits for the socket to connect or until it times out. Returns `bool` indicating whether
+connection has been established (`true`) or whether the operation has timed out or another
+error has occurred (`false`). Returns immediatelly when the socket is already in connected
+state.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QAbstractSocket::waitForConnected()`][qtdoc-qabstractsocket-waitForConnected]
+for details.
+
+```cpp
+QCoro::Task<bool> QCoroAbstractSocket::waitForConnected(int timeout_msecs = 30'000);
+QCoro::Task<bool> QCoroAbstractSocket::waitForConnected(std::chrono::milliseconds timeout);
+```
+
+## `waitForDisconnected()`
+
+Waits for the socket to disconnect from the server or until the operation times out.
+Returns immediatelly if the socket is not in connected state.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QAbstractSocket::waitForDisconnected()`][qtdoc-qabstractsocket-waitForDisconnected]
+for details.
+
+```cpp
+QCoro::Task<bool> QCoroAbstractSocket::waitForDisconnected(timeout_msecs = 30'000);
+QCoro::Task<bool> QCoroAbstractSocket::waitForDisconnected(std::chrono::milliseconds timeout);
+```
+
+## `connectToHost()`
+
+`QCoroAbstractSocket` provides an additional method called `connectToHost()` which is equivalent
+to calling `QAbstractSocket::connectToHost()` followed by `QAbstractSocket::waitForConnected()`. This
+operation is co_awaitable as well.
+
+If the timeout is -1, the operation will never time out.
+
+See the documentation for [`QAbstractSocket::connectToHost()`][qtdoc-qabstractsocket-connectToHost] and
+[`QAbstractSocket::waitForConnected()`][qtdoc-qabstractsocket-waitForConnected] for details.
+
+```cpp
+QCoro::Task<bool> QCoroAbstractSocket::connectToHost(const QHostAddress &address, quint16 port,
+ QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+QCoro::Task<bool> QCoroAbstractSocket::connectToHost(const QString &hostName, quint16 port,
+ QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::AnyIPProtocol,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+```
+
+## Examples
+
+```cpp
+{% include "../../examples/qtcpsocket.cpp" %}
+```
+
+[qtdoc-qiodevice]: https://doc.qt.io/qt-5/qiodevice.html
+[qtdoc-qtcpsocket]: https://doc.qt.io/qt-5/qtcpsocket.html
+[qtdoc-qudpsocket]: https://doc.qt.io/qt-5/qudpsocket.html
+[qtdoc-qabstractsocket]: https://doc.qt.io/qt-5/qabstractsocket.html
+[qtdoc-qabstractsocket-connectToServer]: https://doc.qt.io/qt-5/qabstractsocket.html#connectToServer
+[qtdoc-qabstractsocket-waitForConnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForConnected
+[qtdoc-qabstractsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForDisconnected
+[qcoro-coro]: ../coro/coro.md
+[qcoro-qcoroiodevice]: ../core/qiodevice.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QLocalSocket
+
+{{ doctable("Network", "QCoroLocalSocket", ("core/qiodevice", "QCoroIODevice")) }}
+
+[`QLocalSocket`][qtdoc-qlocalsocket] has several potentially asynchronous operations
+in addition to reading and writing, which are provided by [`QIODevice`][qtdoc-qiodevice]
+baseclass and can be used with coroutines thanks to QCoro's [`QCoroIODevice`][qcoro-qcoroiodevice].
+Those operations are connecting to and disconnecting from the server.
+
+Since `QLocalSocket` doesn't provide the ability to `co_await` those operations, QCoro provides
+ a wrapper calss `QCoroLocalSocket`. To wrap a `QLocalSocket` object into the `QCoroLocalSocket`
+ wrapper, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroLocalSocket qCoro(QLocalSocket &);
+QCoroLocalSocket qCoro(QLocalSocket *);
+```
+
+Same as `QLocalSocket` is a subclass of `QIODevice`, `QCoroLocalSocket` subclasses
+[`QCoroIODevice`][qcoro-qcoroiodevice], so it also provides the awaitable interface for selected
+`QIODevice` functions. See [`QCoroIODevice`][qcoro-qcoroiodevice] documentation for details.
+
+## `waitForConnected()`
+
+Waits for the socket to connect or until it times out. Returns `bool` indicating whether
+connection has been established (`true`) or whether the operation has timed out or another
+error has occurred (`false`). Returns immeditelly if the socket is already in connected
+state.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QLocalSocket::waitForConnected()`][qtdoc-qlocalsocket-waitForConnected]
+for details.
+
+```cpp
+QCoro::Task<bool> QCoroLocalSocket::waitForConnected(int timeout_msecs = 30'000);
+QCoro::Task<bool> QCoroLocalSocket::waitForConnected(std::chrono::milliseconds timeout);
+```
+
+## `waitForDisconnected()`
+
+Waits for the socket to disconnect from the server or until the operation times out.
+Returns immediatelly if the socket is not in connected state.
+
+If the timeout is -1, the operation will never time out.
+
+See documentation for [`QLocalSocket::waitForDisconnected()`][qtdoc-qlocalsocket-waitForDisconnected]
+for details.
+
+```cpp
+QCoro::Task<bool> QCoroLocalSocket::waitForDisconnected(timeout_msecs = 30'000);
+QCoro::Task<bool> QCoroLocalSocket::waitForDisconnected(std::chrono::milliseconds timeout);
+```
+
+## `connectToServer()`
+
+`QCoroLocalSocket` provides an additional method called `connectToServer()` which is equivalent
+to calling `QLocalSocket::connectToServer()` followed by `QLocalSocket::waitForConnected()`. This
+operation is co_awaitable as well.
+
+If the timeout is -1, the operation will never time out.
+
+See the documentation for [`QLocalSocket::connectToServer()`][qtdoc-qlocalsocket-connectToServer] and
+[`QLocalSocket::waitForConnected()`][qtdoc-qlocalsocket-waitForConnected] for details.
+
+```cpp
+QCoro::Task<bool> QCoroLocalSocket::connectToServer(QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+QCoro::Taks<bool> QCoroLocalSocket::connectToServer(const QString &name, QIODevice::OpenMode openMode = QIODevice::ReadOnly,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+```
+
+## Examples
+
+```cpp
+QCoro::Task<QByteArray> requestDataFromServer(const QString &serverName) {
+ QLocalSocket socket;
+ if (!co_await qCoro(socket).connectToServer(serverName)) {
+ qWarning() << "Failed to connect to the server";
+ co_return QByteArray{};
+ }
+
+ socket.write("SEND ME DATA!");
+
+ QByteArray data;
+ while (!data.endsWith("\r\n.\r\n")) {
+ data += co_await qCoro(socket).readAll();
+ }
+
+ co_return data;
+}
+```
+
+[qtdoc-qiodevice]: https://doc.qt.io/qt-5/qiodevice.html
+[qtdoc-qlocalsocket]: https://doc.qt.io/qt-5/qlocalsocket.html
+[qtdoc-qlocalsocket-connectToServer]: https://doc.qt.io/qt-5/qlocalsocket.html#connectToServer
+[qtdoc-qlocalsocket-waitForConnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForConnected
+[qtdoc-qlocalsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForDisconnected
+[qcoro-coro]: ../coro/coro.md
+[qcoro-qcoroiodevice]: ../core/qiodevice.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QNetworkReply
+
+{{ doctable("Network", "QCoroNetworkReply", ("network/qiodevice", "QCoroIODevice")) }}
+
+[`QNetworkReply`][qdoc-qnetworkreply] has two asynchronous aspects: one is waiting for the
+reply to finish, and one for reading the response data as they arrive. QCoro supports both.
+`QNetworkReply` is a subclass of [`QIODevice`][qdoc-qiodevice], so you can leverage all the
+features of [`QCoroIODevice`][qcoro-iodevice] to asynchronously read data from the underlying
+`QIODevice` using coroutines.
+
+To wait for the reply to finish, one can simply `co_await` the reply object:
+
+```cpp
+QNetworkAccessManager nam;
+auto *reply = co_await nam.get(request);
+```
+
+The QCoro frameworks allows `co_await`ing on [QNetworkReply][qdoc-qnetworkreply] objects. The
+co-awaiting coroutine is suspended, until [`QNetworkReply::finished()`][qdoc-qnetworkreply-finished]
+signal is emitted.
+
+To make it work, include `QCoroNetworkReply` in your implementation.
+
+```cpp
+{% include "../../examples/qnetworkreply.cpp" %}
+```
+
+[qdoc-qnetworkreply]: https://doc.qt.io/qt-5/qnetworkreply.html
+[qdoc-qnetworkreply-finished]: https://doc.qt.io/qt-5/qnetworkreply.html#finished
+[qdoc-qiodevice]: https://doc.qt.io/qt-5/qiodevice.html
+[qcoro-iodevice]: ../core/qiodevice.md
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QTcpServer
+
+{{ doctable("Network", "QCoroTcpServer") }}
+
+[`QTcpServer`][qtdoc-qtcpserver] really only has one asynchronous operation worth `co_await`ing, and that's
+`waitForNewConnection()`.
+
+Since `QTcpServer` doesn't provide the ability to `co_await` those operations, QCoro provides
+ a wrapper class `QCoroTcpServer`. To wrap a `QTcpServer` object into the `QCoroTcpServer`
+ wrapper, use [`qCoro()`][qcoro-coro]:
+
+```cpp
+QCoroTcpServer qCoro(QTcpServer &);
+QCoroTcpServer qCoro(QTcpServer *);
+```
+
+## `waitForNewConnection()`
+
+Waits until a new incoming connection is available or until it times out. Returns pointer to `QTcpSocket` or
+`nullptr` if the operation timed out or another error has occured.
+
+If the timeout is -1 the operation will never time out.
+
+See documentation for [`QTcpServer::waitForNewConnection()`][qtdoc-qtcpserver-waitForNewConnection]
+for details.
+
+```cpp
+QCoro::Task<QTcpSocket *> QCoroTcpServer::waitForNewConnection(int timeout_msecs = 30'000);
+QCoro::Task<QTcpSocket *> QCoroTcpServer::waitForNewConnection(std::chrono::milliseconds timeout);
+```
+
+## Examples
+
+```cpp
+{% include "../../examples/qtcpserver.cpp" %}
+```
+
+[qtdoc-qtcpserver]: https://doc.qt.io/qt-5/qtcpserver.html
+[qtdoc-qtcpserver-waitForNewConnection]: https://doc.qt.io/qt-5/qtcpserver.html#waitForNewConnection
+[qcoro-coro]: ../coro/coro.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QML Module
+
+The `QML` module contains coroutine-friendly wrappers for
+[QtQml][qtdoc-qml] classes.
+
+## CMake Usage
+
+```cmake
+find_package(QCoro6 COMPONENTS Qml)
+...
+target_link_libraries(my-target QCoro::Qml)
+```
+
+## QMake Usage
+
+```
+QT += QCoroQml
+```
+
+## Type registration
+
+To use types defined in QCoroQml, you need to call the `QCoro::Qml::registerTypes` function before loading the QML.
+
+```C++
+int main() {
+ ...
+ QCoro::Qml::registerTypes();
+ ...
+}
+```
+
+[qtdoc-qml]: https://doc.qt.io/qt-5/qml-index.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QmlTask
+
+{{ doctable("Qml", "QCoroQmlTask") }}
+
+QmlTask allows to return [QCoro::Task][qcoro-task]s directly to QML.
+It can be constructed from any [QCoro::Task][qcoro-task] that returns a value that can be converted to a [QVariant][qdoc-qml].
+
+```C++
+#include <QCoroQml>
+#include <QCoroQmlTask>
+
+int main()
+{
+ ...
+ qmlRegisterType<Example>("io.me.qmlmodule", 1, 0, "Example");
+ QCoro::Qml::registerTypes();
+ ...
+}
+
+class Example : public QObject
+{
+ Q_OBJECT
+
+ ...
+
+public:
+ Q_INVOKABLE QCoro::QmlTask fetchValue(const QString &name) const
+ {
+ return database->fetchValue(name);
+ // Returns QCoro::Task<QString>
+ }
+}
+```
+
+QmlTasks can call a JavaScript function when they complete:
+```QML
+Example {
+ Component.onCompleted: {
+ fetchValue("key").then((result) => console.log("Result", result))
+ }
+}
+```
+
+They can also set properties when the value is available:
+```QML
+import QCoro 0
+import io.me.qmlmodule 1.0
+
+Item {
+ Example { id: store }
+
+ Label {
+ text: store.fetchValue("key").await().value
+ }
+}
+```
+
+
+[qdoc-qml]: https://doc.qt.io/qt-5/qvariant.html
+[qcoro-task]: ../coro/task.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# ImageProvider
+
+{{ doctable("Quick", "QCoroImageProvider") }}
+
+Coroutines based implementation of [`QQuickImaqeProvider`][qdoc-imageprovider].
+
+To use `QCoro::ImageProvider`, you need to create a subclass of it, and implement the `asyncRequestImage` function, as shown in the example below:
+```cpp
+#include <QCoro/QCoroImageProvider>
+
+class IconImageProvider : public QCoro::ImageProvider
+{
+public:
+ QCoro::Task<QImage> asyncRequestImage(const QString &id, const QSize &) override;
+};
+```
+
+The subclass [can be registered with a `QQmlEngine`][qdoc-addimageprovider] like any `QQuickImageProvider` subclass.
+
+[qdoc-addimageprovider]: https://doc.qt.io/qt-5/qqmlengine.html#addImageProvider
+[qdoc-imageprovider]: https://doc.qt.io/qt-5/qquickimageprovider.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Quick Module
+
+The `Quick` module contains coroutine-friendly wrappers for
+[QtQuick][qtdoc-qtquick] classes.
+
+## CMake Usage
+
+```cmake
+find_package(QCoro6 COMPONENTS Quick)
+...
+target_link_libraries(my-target QCoro::Quick)
+```
+
+## QMake Usage
+
+```
+QT += QCoroQuick
+```
+
+[qtdoc-qtquick]: https://doc.qt.io/qt-5/qtquick-index.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# Test Module
+
+The `Test` module contains coroutine-friendly versions of tests macros
+from the [QtTest][qdoc-qtest] module.
+
+## CMake Usage
+
+```cmake
+find_package(QCoro6 COMPONENTS Test)
+...
+target_link_libraries(my-target QCoro::Test)
+```
+
+## QMake Usage
+
+```
+QT += QCoroTest
+```
+
+## Test Macros
+
+The module contains a bunch of test macros that behave identically to their QtTest
+counterparts, the only difference being that they can be used inside a coroutine.
+
+```cpp
+#include <QCoroTest>
+```
+
+* [`QCORO_COMPARE(actual, expected)`][qdoc-qcompare]
+* [`QCORO_EXPECT_FAIL(dataIndex, comment, mode)`][qdoc-qexpect-fail]
+* [`QCORO_FAIL(message)`][qdoc-qfail]
+* [`QCORO_SKIP(description)`][qdoc-qskip]
+* [`QCORO_TEST(data, testElement)`][qdoc-qtest]
+* [`QCORO_TRY_COMPARE(actual, expected)`][qdoc-qtry-compare]
+* [`QCORO_TRY_COMPARE_WITH_TIMEOUT(actual, expected, timeout)`][qdoc-qtry-compare-with-timeout]
+* [`QCORO_TRY_VERIFY2(condition, message)`][qdoc-qtry-verify2]
+* [`QCORO_TRY_VERIFY(condition)`][qdoc-qtry-verify]
+* [`QCORO_TRY_VERIFY2_WITH_TIMEOUT(condition, message, timeout)`][qdoc-qtry-verify2-with-timeout]
+* [`QCORO_TRY_VERIFY_WITH_TIMEOUT(condition, timeout)`][qdoc-qtry-verify-with-timeout]
+* [`QCORO_VERIFY2(condition, message)`][qdoc-qverify2]
+* [`QCORO_VERIFY(condition)`][qdoc-qverify]
+* [`QCORO_VERIFY_EXCEPTION_THROWN(expression, exceptionType)`][qdoc-qverify-exception-thrown]
+* [`QCORO_VERIFY_THROWS_EXCEPTION(exceptionType, ...)`][qdoc6-qverify-throws-exception]
+
+
+[qdoc-qtest]: https://doc.qt.io/qt-5/qttest-index.html
+[qdoc-qcompare]: https://doc.qt.io/qt-5/qtest.html#QCOMPARE
+[qdoc-qexpect-fail]: https://doc.qt.io/qt-5/qtest.html#QEXPECT_FAIL
+[qdoc-qfail]: https://doc.qt.io/qt-5/qtest.html#QFAIL
+[qdoc-qskip]: https://doc.qt.io/qt-5/qtest.html#QSKIP
+[qdoc-qtest]: https://doc.qt.io/qt-5/qtest.html#QTEST
+[qdoc-qtry-compare]: https://doc.qt.io/qt-5/qtest.html#QTRY_COMPARE
+[qdoc-qtry-compare-with-timeout]: https://doc.qt.io/qt-5/qtest.html#QTRY_COMPARE_WITH_TIMEOUT
+[qdoc-qtry-verify2]: https://doc.qt.io/qt-5/qtest.html#QTRY_VERIFY2
+[qdoc-qtry-verify]: https://doc.qt.io/qt-5/qtest.html#QTRY_VERIFY
+[qdoc-qtry-verify2-with-timeout]: https://doc.qt.io/qt-5/qtest.html#QTRY_VERIFY2_WITH_TIMEOUT
+[qdoc-qtry-verify-with-timeout]: https://doc.qt.io/qt-5/qtest.html#QTRY_VERIFY_WITH_TIMEOUT
+[qdoc-qverify2]: https://doc.qt.io/qt-5/qtest.html#QVERIFY2
+[qdoc-qverify]: https://doc.qt.io/qt-5/qtest.html#QVERIFY
+[qdoc-qverify-exception-thrown]: https://doc.qt.io/qt-5/qtest.html#QVERIFY_EXCEPTION_THROWN
+[qdoc6-qverify-throws-exception]: https://doc.qt.io/qt-6/qtest.html#QVERIFY_THROWS_EXCEPTION
+
+
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# WebSockets Module
+
+The `WebSockets` module contains coroutine-friendly wrapper for
+[QtWebSockets][qtdoc-websockets] classes.
+
+## CMake Usage
+
+```
+find_package(QCoro6 COMPONENTS WebSockets)
+
+...
+target_link_libraries(my-target QCoro::WebSockets)
+```
+
+## QMake Usage
+
+```
+QT += QCoroWebSockets
+```
+
+[qtdoc-websockets]: https://doc.qt.io/qt-5/qtwebsockets-index.html
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QWebSocket
+
+{{ doctable("WebSockets", "QCoroWebSocket") }}
+
+
+[`QWebSocket`][qtdoc-qwebsocket] provides a client socket to connect to a WebSocket server, which
+several asynchronous operations that could be used as coroutines. The `QCoroWebSocket` wrapper
+provides exactly this. Use `qCoro()` to wrap an existing instance of `QWebSocket` to become
+`QCoroWebSocket`:
+
+```cpp
+QCoroWebSocket qCoro(QWebSocket &);
+QCoroWebSocket qCoro(QWebSocket *);
+```
+
+## open()
+
+Opens connection to the WebSocket server and waits until the connection is established, or
+a timeout occurs. Resolves to `true` when the connection was successfully established, or
+`false` if the process has timed out. If the timeout is `-1`, the operation will never time out.
+
+See documentation for [`QWebSocket::open(const QUrl &)`][qtdoc-qwebsocket-open-qurl] and
+[`QWebSocket::open(const QNetworkRequest &)`][qtdoc-qwebsocket-open-qnetworkrequest] for details.
+
+```cpp
+QCoro::Task<bool> QCoroWebSocket::open(const QUrl &url, std::chrono::milliseconds timeout);
+QCoro::Task<bool> QCoroWebSocket::open(const QNetworkRequest &request, std::chrono::milliseconds timeout);
+```
+
+## ping()
+
+Sends the given payload to the server and waits for response. Returns the roundtrip time
+elapsed, or empty value if the operation has timed out. If the timeout is set to `-1`, the
+operation will never time out.
+
+See documentation for [`QWebSocket::ping()`][qtdoc-qwebsocket-ping] for details.
+
+```cpp
+QCoro::Task<std::optional<std::chrono::milliseconds>> ping(const QByteArray &payload, std::chrono::milliseconds timeout);
+```
+
+## binaryFrames()
+
+Returns an [asynchronous generator][qcoro-async-generator] that will yield frame data whenever
+they arrive. More specifically, the generator will yield a tuple of `QByteArray` containing the
+frame data, and a `boolean` value, indicating whether this is the last frame of a message,
+allowing to receive large messages in smaller frame-sized chunks.
+
+Note that the generator will never terminate, unless the socket is disconnected or unless the
+time elapsed between frames gets over the specified `timeout`. If the `timeout` is `-1`, the
+generator will wait for new frames indefinitely.
+
+See documentation for the [`QWebSocket::binaryFrameReceived()`][qtdoc-qwebsocket-binary-frame-received] signal for details.
+
+```cpp
+QCoro::AsyncGenerator<std::tuple<QByteArray, bool>> QCoroWebSocket::binaryFrames(std::chrono::milliseconds timeout);
+```
+
+!!! question "Why generator instead of simply co_awaiting next frame?"
+ It's logical to ask why does this function return a generator, rather than simply
+ returning the received frame, the user would then go on to `co_await` the next
+ frame etc. Unlike e.g. `QIODevice`-based classes, the `QWebSocket` is not buffered,
+ that is any frame that arrives when no-one is connected to the `binaryFrameReceived()`
+ signal is simply dropped and cannot be retrieved later. Therefore we need to use a
+ generator API, which will buffer all received frames for as long as the generator
+ object exists and provide them through the familiar iterator-like interface.
+
+
+Here's an example coroutine that assembles messages from incoming frames and emits a singal
+whenever a full message is assembled (don't use this in real code, use the `binaryMessages()`
+coroutine to receive complete messages).
+
+```cpp
+QCoro::Task<> MessageReceived::receive() {
+ QByteArray message;
+ QCORO_FOREACH(const auto &[frame, isLast], qCoro(mWebSocket).binaryFrames()) {
+ message.append(frame);
+ if (isLast) {
+ Q_EMIT messageAssembled(message);
+ message.clear();
+ }
+ }
+ qDebug() << "Socket disconnected!";
+}
+
+```
+
+## textFrames()
+
+Behaves exactly like [`binaryFrames()`](#binaryframes), except that it expects the frame
+to contain text data.
+
+```cpp
+QCoro::AsyncGenerator<std::tuple<QString, bool>> QCoroWebSocket::textFrames(std::chrono::milliseconds timeout);
+```
+
+## binaryMessages()
+
+Returns an [asynchronous generator][qcoro-async-generator] that will yield a binary message whenever
+it arrives.
+
+Note that the generator will never terminate, unless the socket is disconnected or unless the
+time elapsed between frames gets over the specified `timeout`. If the `timeout` is `-1`, the
+generator will wait for new frames indefinitely.
+
+See documentation for the [`QWebSocket::binaryMessageReceived()`][qtdoc-qwebsocket-binary-message-received] signal for details.
+
+```cpp
+QCoro::AsyncGenerator<QByteArray> QCoroWebSocket::binaryMessages(std::chrono::milliseconds timeout);
+```
+
+## textMessages()
+
+Behaves exactly like [`binaryMessages()`](#binarymessages), except that it expects the message
+to be text message.
+
+
+```cpp
+QCoro::AsyncGenerator<QString> QCoroWebSocket::textMessage(std::chrono::milliseconds timeout);
+```
+
+
+[qtdoc-qwebsocket]: https://doc.qt.io/qt-5/qwebsocket.html
+[qtdoc-qwebsocket-open-qurl]: https://doc.qt.io/qt-5/qwebsocket.html#open
+[qtdoc-qwebsocket-open-qnetworkrequest]: https://doc.qt.io/qt-5/qwebsocket.html#open-1
+[qtdoc-qwebsocket-ping]: https://doc.qt.io/qt-5/qwebsocket.html#ping
+
+[qcoro-async-generator]: ../coro/asyncgenerator.md
--- /dev/null
+<!--
+SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+
+SPDX-License-Identifier: GFDL-1.3-or-later
+-->
+
+# QWebSocketServer
+
+{{ doctable("WebSockets", "QCoroWebSocketServer") }}
+
+QCoro provides a wrapper for the [`QWebSocketServer`][qtdoc-qwebsocketserver] class which allows
+user to asynchronously co_await incoming connections. To wrap a `QWebSocketServer` instance, use
+the `qCoro()` overload from the `QCoroWebSocketServer` include header.
+
+
+```cpp
+QCoroWebSocketServer qCoro(QWebSocketServer &);
+QCoroWebSocketServer qCoro(QWebSocketServer *);
+```
+
+## nextPendingConnection()
+
+Suspends the awaiter until a new incoming connection becomes available or until the specified timeout.
+If the specified timeout is `-1`, the operation will never time out.
+
+Returns a pointer to [`QWebSocket`][qtdoc-qwebsocket] representing the new connection, or a null pointer if the operation
+times out, the server is not [`listen()`][qtdoc-qwebsocketserver-listen]ining for incoming connections.
+
+```cpp
+QCoro::Task<QWebSocket *> QCoroWebSocketServer::nextPendingConnection(std::chrono::milliseconds timeout);
+```
+
+Note that [`pauseAccepting()`][qtdoc-qwebsocketserver-pauseAccepting] doesn't resume any awaiting
+coroutines.
+
+```cpp
+QCoro::Task<> listen(QWebSocketServer *server) {
+ server->listen();
+ while (auto socket = std::unique_ptr<QWebSocket>(co_await qCoro(server).nextPendingConnection());
+ socket != nullptr)
+ {
+ handleConnection(std::move(socket));
+ }
+}
+```
+
+[qtdoc-qwebsocketserver]: https://doc.qt.io/qt-5/qwebsocketserver.html
+[qtdoc-qwebsocketserver-pauseAccepting]: https://doc.qt.io/qt-5/qwebsocketserver.html#pauseAccepting
+[qtdoc-qwebsocketserver-listen]: https://doc.qt.io/qt-5/qwebsocketserver.html#listen
+[qtdoc-qwebsocket]: https://doc.qt.io/qt-5/qwebsocket.html
--- /dev/null
+# QCoro Release Checklist
+
+1. Ensure all CI builds for current `main` pass
+1. Create Release Announcement in `docs/news/`
+1. Update `docs/changelog.md`
+1. Bump version in `qcoro_VERSION` in CMakeLists.txt to `X.Y.Z`
+1. Commit release announcement and version bump as `QCoro release X.Y.Z`
+1. Tag the release commit: `git tag -s vX.Y.Z -m "QCoro X.Y.Z"`
+1. Bump version in `qcoro_VERSION` in CMakeLists.txt to `X.Y.80`
+1. Commit version bump as `Bump version to X.Y.80 (X.(Y+1).0 development)`
+1. Push tag and commits: `git push --tags origin main`
+1. [Create release in Github](https://github.com/danvratil/qcoro/releases/new)
+1. Submit PR to update QCoro in [ConanCenter](https://github.com/conan-io/conan-center-index/)
+1. Submit PRs to update QCoro in [vcpkg](https://github.com/microsoft/vcpkg)
+1. Copy the release announcement to my blog to publicize the release on Planet KDE
+1. Post an announcement to Twitter
+1. Add any extra step taken during last release and not included in this list
--- /dev/null
+.doctable {
+ width: 100%
+}
+
+.doctable .md-typeset__table {
+ width: 100%
+}
+
+.doctable td {
+ width: 100%
+}
+
+.doctable .md-typeset__table table th {
+ vertical-align: middle
+}
--- /dev/null
+add_subdirectory(basics)
+if (QCORO_WITH_QTDBUS)
+ add_subdirectory(dbus)
+endif()
+if (QCORO_WITH_QTNETWORK)
+ add_subdirectory(network)
+endif()
+if (QCORO_QT_HAS_COMPAT_ABI)
+ add_subdirectory(future)
+endif()
+add_subdirectory(iodevice)
+add_subdirectory(timer)
+add_subdirectory(chained)
+add_subdirectory(background-task)
+
--- /dev/null
+add_executable(background-task-example)
+target_sources(background-task-example PRIVATE main.cpp)
+target_link_libraries(background-task-example
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}Core
+ Qt${QT_VERSION_MAJOR}::Core
+)
+set_target_defaults(background-task-example)
+
--- /dev/null
+#include <QCoreApplication>
+#include <QFile>
+
+#include "qcorotask.h"
+#include "qcorotimer.h"
+#include "qcoroiodevice.h"
+
+using namespace std::chrono_literals;
+
+// We could use std::stop_token, but it is not supported by AppleClang
+struct Stop {
+public:
+ void requestStop() {
+ mShouldStop = true;
+ }
+
+ bool stopRequested() const {
+ return mShouldStop;
+ }
+private:
+ bool mShouldStop = false;
+};
+
+auto backgroundTask(const Stop &stop) -> QCoro::Task<> {
+ qDebug() << "Task: Background task started, waiting for event loop";
+ co_await QCoro::sleepFor(0ms);
+ qDebug() << "Task: Event loop is running";
+ QFile file(QStringLiteral("/dev/stdin"));
+ bool openResult = file.open(QIODevice::ReadOnly | QIODevice::Unbuffered);
+ Q_UNUSED(openResult);
+ while (!stop.stopRequested()) {
+ qDebug() << "Task: Waiting for input...";
+ const auto result = co_await qCoro(file).readLine(1024, 5s);
+ if (!result.isEmpty()) {
+ qDebug() << "Task: Read line:" << result;
+ } else {
+ qDebug() << "Task: Timeout!";
+ }
+ }
+ qDebug() << "Task: Backround task stopped";
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication app(argc, argv);
+
+ Stop stop;
+ auto bgTask = backgroundTask(stop);
+ QObject::connect(&app, &QCoreApplication::aboutToQuit, [&stop] {
+ qDebug() << "App: Requesting background task to stop";
+ stop.requestStop();
+ });
+ QTimer::singleShot(500ms, &app, []() {
+ qDebug() << "App: Stopping application";
+ QCoreApplication::instance()->quit();
+ });
+
+ // Run the app
+ qDebug() << "App: Starting application event loop";
+ const auto result = app.exec();
+ qDebug() << "App: Application event loop stopped";
+
+ // Wait for the background task to complete
+ qDebug() << "App: Waiting for background task to complete";
+ QCoro::waitFor(bgTask);
+ qDebug() << "App: Background task completed";
+
+ return result;
+}
--- /dev/null
+add_executable(await-sync-string await-sync-string.cpp)
+target_link_libraries(await-sync-string QCoro${QT_VERSION_MAJOR}::Coro)
+set_target_defaults(await-sync-string)
+
+add_executable(await-async-string await-async-string.cpp)
+target_link_libraries(await-async-string QCoro${QT_VERSION_MAJOR}::Coro Qt${QT_VERSION_MAJOR}::Core)
+set_target_defaults(await-async-string)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoro/coroutine.h"
+
+#include <QCoreApplication>
+#include <QMetaObject>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <string>
+
+using namespace std::chrono_literals;
+
+class FutureString : public QObject {
+ Q_OBJECT
+public:
+ explicit FutureString(const QString &str) : mStr(str) {
+ QTimer::singleShot(1s, this, [this]() {
+ mReady = true;
+ Q_EMIT ready();
+ });
+ }
+
+ bool isReady() const {
+ return mReady;
+ }
+ QString result() const {
+ return mStr;
+ }
+Q_SIGNALS:
+ void ready();
+
+private:
+ bool mReady = false;
+ QString mStr;
+};
+
+// Awaiter is a concept that provides the await_* methods below, which are used by the
+// co_await expression.
+// Type is Awaitable if it supports the `co_await` operator.
+//
+// When compiler sees a `co_await <expr>`, it first tries to obtain an Awaitable type for
+// the expression result result type:
+// - first by checking if the current coroutine's promise type has `await_transform()`
+// that for given type returns an Awaitable
+// - if it does not have await_transform, it treats the result type as awaitable.
+// Thus, if the current promise type doesn't have compatible `await_transform()` and the
+// type itself is not Awaitable, it cannot be `co_await`ed.
+//
+// If the Awaitable object has `operator co_await` overload, it calls it to obtain the
+// Awaiter object. Otherwise the Awaitable object is used as an Awaiter.
+//
+class FutureStringAwaiter {
+public:
+ explicit FutureStringAwaiter(const std::shared_ptr<FutureString> value) noexcept
+ : mFuture(value) {
+ std::cout << "FutureStringAwaiter constructed." << std::endl;
+ }
+ ~FutureStringAwaiter() {
+ std::cout << "FutureStringAwaiter destroyed." << std::endl;
+ }
+
+ // Called by compiler when starting co_await to check whether the awaited object is by any
+ // chance already ready, so that we could avoid the suspend-resume dance.
+ bool await_ready() noexcept {
+ std::cout << "FutureStringAwaiter::await_ready() called." << std::endl;
+ return mFuture->isReady();
+ }
+ // Called to tell us that the awaiting coroutine was suspended.
+ // We use the awaitingCoroutine handle to resume the suspended coroutine once the
+ // co_awaited coroutine is finished.
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ std::cout << "FutureStringAwaiter::await_suspend() called." << std::endl;
+ QObject::connect(mFuture.get(), &FutureString::ready,
+ [awaitingCoroutine]() mutable { awaitingCoroutine.resume(); });
+ }
+ // Called when the co_awaiting coroutine is resumed. Returns result of the
+ // co_awaited expression.
+ QString await_resume() noexcept {
+ std::cout << "FutureStringAwaiter::await_resume() called." << std::endl;
+ return mFuture->result();
+ }
+
+private:
+ std::shared_ptr<FutureString> mFuture;
+};
+
+class FutureStringAwaitable {
+public:
+ FutureStringAwaitable(const std::shared_ptr<FutureString> value) noexcept : mFuture(value) {
+ std::cout << "FutureStringAwaitable constructed." << std::endl;
+ }
+
+ ~FutureStringAwaitable() {
+ std::cout << "FutureStringAwaitable destroyed." << std::endl;
+ }
+
+ FutureStringAwaiter operator co_await() {
+ std::cout << "FutureStringAwaitable::operator co_await() called." << std::endl;
+ return FutureStringAwaiter{mFuture};
+ }
+
+private:
+ std::shared_ptr<FutureString> mFuture;
+};
+
+class VoidPromise {
+public:
+ explicit VoidPromise() {
+ std::cout << "VoidPromise constructed." << std::endl;
+ }
+
+ ~VoidPromise() {
+ std::cout << "VoidPromise destroyed." << std::endl;
+ }
+
+ struct promise_type {
+ explicit promise_type() {
+ std::cout << "VoidPromise::promise_type constructed." << std::endl;
+ }
+
+ ~promise_type() {
+ std::cout << "VoidPromise::promise_type destroyed." << std::endl;
+ }
+
+ // Says whether the coroutine body should be executed immediately (`suspend_never`)
+ // or whether it should be executed only once the coroutine is co_awaited.
+ std::suspend_never initial_suspend() const noexcept {
+ return {};
+ }
+ // Says whether the coroutine should be suspended after returning a result
+ // (`suspend_always`) or whether it should just end and the frame pointer and everything
+ // should be destroyed.
+ std::suspend_never final_suspend() const noexcept {
+ return {};
+ }
+
+ // Called by the compiler during initial coroutine setup to obtain the object that
+ // will be returned from the coroutine when it is suspended.
+ // Sicne this is a promise type for VoidPromise, we return a VoidPromise.
+ VoidPromise get_return_object() {
+ std::cout << "VoidPromise::get_return_object() called." << std::endl;
+ return VoidPromise();
+ }
+
+ // Called by the compiler when an exception propagates from the coroutine.
+ // Alternatively, we could declare `set_exception()` which the compiler would
+ // call instead to let us handle the exception (e.g. propagate it)
+ void unhandled_exception() {
+ std::terminate();
+ }
+
+ // The result of the promise. Since our promise is void, we must implement `return_void()`.
+ // If our promise would be returning a value, we would have to implement `return_value()`
+ // instead.
+ void return_void() const noexcept {};
+
+ FutureStringAwaitable await_transform(const std::shared_ptr<FutureString> &future) {
+ std::cout << "VoidPromise::await_transform called." << std::endl;
+ return FutureStringAwaitable{future};
+ }
+ };
+};
+
+std::shared_ptr<FutureString> regularFunction() {
+ return std::make_shared<FutureString>(QStringLiteral("Hello World!"));
+}
+
+// This function co_awaits, therefore it's a co-routine and must
+// have a promise type to return to the caller.
+VoidPromise myCoroutine() {
+ // 1. Compiler creates a new coroutine frame `f`
+ // 2. Compiler obtains a return object from the promise.
+ // The promise is of type `std::coroutine_traits<CurrentFunctionReturnType>::promise_type`,
+ // which is `CurrentFunctionReturnType::promise_type` (if there is no specialization for
+ // `std::coroutine_traits<CurrentFunctionReturnType>`)
+ // 3. Compiler starts execution of the coroutine body by calling `resume()` on the
+ // current coroutine's std::coroutine_handle (obtained from the promise by
+ // `std::coroutine_handle<decltype(f->promise)>::from_promise(f->promise)
+
+ std::cout << "myCoroutine() started." << std::endl;
+ const auto result = co_await regularFunction();
+ std::cout << "Result successfully co_await-ed: " << result.toStdString() << std::endl;
+
+ qApp->quit();
+}
+
+int main(int argc, char **argv) {
+
+ QCoreApplication app(argc, argv);
+ QMetaObject::invokeMethod(&app, myCoroutine);
+ QTimer t;
+ QObject::connect(&t, &QTimer::timeout, &app, []() { std::cout << "Tick" << std::endl; });
+ t.start(100ms);
+ return app.exec();
+
+ return 0;
+}
+
+#include "await-async-string.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoro/coroutine.h"
+
+#include <iostream>
+#include <string>
+
+// Awaiter is a concept that provides the await_* methods below, which are used by the
+// co_await expression.
+// Type is Awaitable if it supports the `co_await` operator.
+//
+// When compiler sees a `co_await <expr>`, it first tries to obtain an Awaitable type for
+// the expression result result type:
+// - first by checking if the current coroutine's promise type has `await_transform()`
+// that for given type returns an Awaitable
+// - if it does not have await_transform, it treats the result type as awaitable.
+// Thus, if the current promise type doesn't have compatible `await_transform()` and the
+// type itself is not Awaitable, it cannot be `co_await`ed.
+//
+// If the Awaitable object has `operator co_await` overload, it calls it to obtain the
+// Awaiter object. Otherwise the Awaitable object is used as an Awaiter.
+//
+class StringAwaiter {
+public:
+ explicit StringAwaiter(const std::string &value) noexcept : mValue(value) {
+ std::cout << "StringAwaiter constructed with value '" << value << "'." << std::endl;
+ }
+ ~StringAwaiter() {
+ std::cout << "StringAwaiter destroyed." << std::endl;
+ }
+
+ bool await_ready() noexcept {
+ std::cout << "StringAwaiter::await_ready() called." << std::endl;
+ return false;
+ }
+ void await_suspend(std::coroutine_handle<>) noexcept {
+ std::cout << "StringAwaiter::await_suspend() called." << std::endl;
+ }
+ std::string await_resume() noexcept {
+ std::cout << "StringAwaiter::await_resume() called." << std::endl;
+ return mValue;
+ }
+
+private:
+ std::string mValue;
+};
+
+class StringAwaitable {
+public:
+ StringAwaitable(std::string str) noexcept : mStr(std::move(str)) {
+ std::cout << "StringAwaitable constructored with value '" << mStr << "'." << std::endl;
+ }
+
+ ~StringAwaitable() {
+ std::cout << "StringAwaitable destroyed." << std::endl;
+ }
+
+ StringAwaiter operator co_await() {
+ std::cout << "StringAwaitable::operator co_await() called." << std::endl;
+ return StringAwaiter{mStr};
+ }
+
+private:
+ std::string mStr;
+};
+
+class VoidPromise {
+public:
+ explicit VoidPromise() {
+ std::cout << "VoidPromise constructed." << std::endl;
+ }
+
+ ~VoidPromise() {
+ std::cout << "VoidPromise destroyed." << std::endl;
+ }
+
+ struct promise_type {
+ explicit promise_type() {
+ std::cout << "VoidPromise::promise_type constructed." << std::endl;
+ }
+
+ ~promise_type() {
+ std::cout << "VoidPromise::promise_type destroyed." << std::endl;
+ }
+
+ // Says whether the coroutine body should be executed immediately (`suspend_never`)
+ // or whether it should be executed only once the coroutine is co_awaited.
+ std::suspend_never initial_suspend() const noexcept {
+ return {};
+ }
+ // Says whether the coroutine should be suspended after returning a result
+ // (`suspend_always`) or whether it should just end and the frame pointer and everything
+ // should be destroyed.
+ std::suspend_never final_suspend() const noexcept {
+ return {};
+ }
+
+ // Called by the compiler during initial coroutine setup to obtain the object that
+ // will be returned from the coroutine when it is suspended.
+ // Sicne this is a promise type for VoidPromise, we return a VoidPromise.
+ VoidPromise get_return_object() {
+ std::cout << "VoidPromise::get_return_object() called." << std::endl;
+ return VoidPromise();
+ }
+
+ // Called by the compiler when an exception propagates from the coroutine.
+ // Alternatively, we could declare `set_exception()` which the compiler would
+ // call instead to let us handle the exception (e.g. propagate it)
+ void unhandled_exception() {
+ std::terminate();
+ }
+
+ // The result of the promise. Since our promise is void, we must implement `return_void()`.
+ // If our promise would be returning a value, we would have to implement `return_value()`
+ // instead.
+ void return_void() const noexcept {};
+
+ StringAwaitable await_transform(std::string str) {
+ std::cout << "VoidPromise::await_transform for string '" << str << "' called."
+ << std::endl;
+ return StringAwaitable{std::move(str)};
+ }
+ };
+};
+
+std::string regularFunction() {
+ return "Hello World!";
+}
+
+// This function co_awaits, therefore it's a co-routine and must
+// have a promise type to return to the caller.
+VoidPromise myCoroutine() {
+ // 1. Compiler creates a new coroutine frame `f`
+ // 2. Compiler obtains a return object from the promise.
+ // The promise is of type `std::coroutine_traits<CurrentFunctionReturnType>::promise_type`,
+ // which is `CurrentFunctionReturnType::promise_type` (if there is no specialization for
+ // `std::coroutine_traits<CurrentFunctionReturnType>`)
+ // 3. Compiler starts execution of the coroutine body by calling `resume()` on the
+ // current coroutine's std::coroutine_handle (obtained from the promise by
+ // `std::coroutine_handle<decltype(f->promise)>::from_promise(f->promise)
+
+ std::cout << "myCoroutine() started." << std::endl;
+ const auto result = co_await regularFunction();
+ std::cout << "Result successfully co_await-ed: " << result << std::endl;
+}
+
+int main() {
+ std::cout << "Calling myCoroutine() from main()." << std::endl;
+ myCoroutine();
+ std::cout << "Returned from myCoroutine() to main()." << std::endl;
+
+ return 0;
+}
--- /dev/null
+add_executable(chained-example main.cpp)
+target_link_libraries(chained-example
+ QCoro${QT_VERSION_MAJOR}Core
+ Qt${QT_VERSION_MAJOR}::Core
+)
+set_target_defaults(chained-example)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorotask.h"
+#include "qcorotimer.h"
+
+#include <QCoreApplication>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+
+using namespace std::chrono_literals;
+
+QCoro::Task<QString> generateRandomString() {
+ std::cout << "GenerateRandomString started" << std::endl;
+ QTimer timer;
+ timer.start(1s);
+ std::cout << "GenerateRandomString \"generating\"..." << std::endl;
+ co_await timer;
+ std::cout << "GenerateRandomString finished \"generating\"" << std::endl;
+
+ std::cout << "GenerateRandomString co_returning to caller" << std::endl;
+ co_return QStringLiteral("RandomString!");
+}
+
+QCoro::Task<qsizetype> generateRandomNumber() {
+ std::cout << "GenerateRandomNumber started" << std::endl;
+ std::cout << "GenerateRandomNumber co_awaiting on generateRandomString()" << std::endl;
+ const QString string = co_await generateRandomString();
+ std::cout << "GenerateRandomNumber successfully co_awaited on generateRandomString() and "
+ "co_returns result"
+ << std::endl;
+ co_return string.size();
+}
+
+QCoro::Task<> logRandomNumber() {
+ std::cout << "LogRandomNumber started" << std::endl;
+ std::cout << "LogRandomNumber co_awaiting on generateRandomNumber()" << std::endl;
+ const int number = co_await generateRandomNumber();
+ std::cout << "Random number for today is: " << number << std::endl;
+
+ qApp->quit();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app{argc, argv};
+ QTimer::singleShot(0, qApp, logRandomNumber);
+ return app.exec();
+}
--- /dev/null
+add_subdirectory(common)
+add_subdirectory(regular-blocking)
+add_subdirectory(coroutine)
+
+
--- /dev/null
+
+# Build the dbus-server as a stand-alone executable to workaround QTBUG-92107
+add_executable(dbusserver dbusserver.cpp)
+set_target_properties(dbusserver
+ PROPERTIES COMPILE_DEFINITIONS STANDALONE
+)
+target_link_libraries(dbusserver
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::DBus
+)
+set_target_defaults(dbusserver)
+
+#-----------------------------------------------------#
+
+set(common_SRCS
+ dbusserver.cpp
+)
+
+add_library(examples-dbus-common STATIC ${common_SRCS})
+set_target_properties(examples-dbus-common PROPERTIES COMPILE_DEFINITIONS SERVER_EXEC_PATH=\"$<TARGET_FILE:dbusserver>\")
+target_include_directories(examples-dbus-common
+ INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
+)
+target_link_libraries(examples-dbus-common
+ PUBLIC
+ Qt${QT_VERSION_MAJOR}::Core
+ PRIVATE
+ Qt${QT_VERSION_MAJOR}::DBus
+)
+set_target_defaults(examples-dbus-common)
+
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "dbusserver.h"
+
+#include <QCoreApplication>
+#include <QDBusConnection>
+#include <QDebug>
+#include <QThread>
+
+#include <thread>
+
+const QString DBusServer::serviceName = QStringLiteral("org.kde.qoro.dbustest");
+const QString DBusServer::objectPath = QStringLiteral("/");
+const QString DBusServer::interfaceName = QStringLiteral("org.kde.qoro.dbuserver");
+
+DBusServer::DBusServer() {
+ qInfo() << "DBusServer started";
+ auto bus = QDBusConnection::sessionBus();
+ bus.registerService(serviceName);
+ bus.registerObject(objectPath, interfaceName, this, QDBusConnection::ExportAllSlots);
+}
+
+QString DBusServer::blockingPing(int seconds) const {
+ qInfo() << "S: Received ping request...";
+ std::this_thread::sleep_for(std::chrono::seconds{seconds});
+ qInfo() << "S: sending PONG response";
+ return QStringLiteral("PONG!");
+}
+
+std::unique_ptr<QProcess> DBusServer::runStadaloneServer() {
+#ifdef SERVER_EXEC_PATH
+ auto process = std::make_unique<QProcess>();
+ process->setProcessChannelMode(QProcess::ForwardedChannels);
+ process->start(QStringLiteral(SERVER_EXEC_PATH), {}, QIODevice::ReadOnly);
+ process->waitForStarted();
+ if (process->state() != QProcess::Running) {
+ qCritical() << "Failed to start server process:" << process->error();
+ }
+ return process;
+#else
+ return {};
+#endif
+}
+
+#ifdef STANDALONE
+int main(int argc, char **argv) {
+ QCoreApplication app(argc, argv);
+ DBusServer server;
+ return app.exec();
+}
+#endif
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QObject>
+#include <QProcess>
+
+#include <memory>
+
+class DBusServer : public QObject {
+ Q_OBJECT
+public:
+ static const QString serviceName;
+ static const QString objectPath;
+ static const QString interfaceName;
+
+ explicit DBusServer();
+
+ static std::unique_ptr<QProcess> runStadaloneServer();
+public Q_SLOTS:
+ QString blockingPing(int seconds) const;
+};
--- /dev/null
+set(dbustest_SRCS
+ main.cpp
+)
+
+add_executable(dbustest-coro ${dbustest_SRCS})
+target_link_libraries(dbustest-coro
+ QCoro${QT_VERSION_MAJOR}DBus
+ examples-dbus-common
+ Threads::Threads
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::DBus
+)
+set_target_defaults(dbustest-coro)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "coroutine.h"
+#include "qcorodbus.h"
+#include "qcorotask.h"
+
+#include "common/dbusserver.h"
+
+#include <QCoreApplication>
+#include <QDBusConnection>
+#include <QDBusInterface>
+#include <QDBusPendingCall>
+#include <QDBusReply>
+#include <QDateTime>
+#include <QDebug>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+#include <thread>
+#include <variant>
+
+using namespace std::chrono_literals;
+
+QCoro::Task<> dbusWorker() {
+ auto bus = QDBusConnection::sessionBus();
+ auto iface = QDBusInterface{DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName, bus};
+ qInfo() << "Sending PING";
+ QDBusReply<QString> response = co_await iface.asyncCall(QStringLiteral("blockingPing"), 1);
+ if (const auto &err = response.error(); err.isValid()) {
+ qWarning() << "DBus call failed:" << err.message();
+ }
+ qInfo() << "Received response:" << response.value();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app(argc, argv);
+ auto process = DBusServer::runStadaloneServer();
+
+ QTimer tickTimer;
+ QObject::connect(&tickTimer, &QTimer::timeout, &app, []() {
+ std::cout << QDateTime::currentDateTime().toString(Qt::ISODateWithMs).toStdString()
+ << " Tick!" << std::endl;
+ });
+ tickTimer.start(400ms);
+
+ QTimer dbusTimer;
+ QObject::connect(&dbusTimer, &QTimer::timeout, &app, dbusWorker);
+ dbusTimer.start(2s);
+
+ return app.exec();
+}
--- /dev/null
+set(dbustest_SRCS
+ main.cpp
+)
+
+add_executable(dbustest ${dbustest_SRCS})
+target_link_libraries(dbustest
+ examples-dbus-common
+ Threads::Threads
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::DBus
+)
+set_target_defaults(dbustest)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "common/dbusserver.h"
+
+#include <QCoreApplication>
+#include <QDBusConnection>
+#include <QDBusInterface>
+#include <QDBusReply>
+#include <QDateTime>
+#include <QDebug>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+#include <thread>
+
+using namespace std::chrono_literals;
+
+void dbusWorker() {
+ auto bus = QDBusConnection::sessionBus();
+ auto iface = QDBusInterface{DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName, bus};
+ qInfo() << "Sending PING";
+ QDBusReply<QString> response = iface.call(QStringLiteral("blockingPing"), 1);
+ if (const auto &err = response.error(); err.isValid()) {
+ qWarning() << "DBus call failed:" << err.message();
+ }
+ qInfo() << "Received response:" << response.value();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app(argc, argv);
+ auto process = DBusServer::runStadaloneServer();
+
+ QTimer tickTimer;
+ QObject::connect(&tickTimer, &QTimer::timeout, &app, []() {
+ std::cout << QDateTime::currentDateTime().toString(Qt::ISODateWithMs).toStdString()
+ << " Tick!" << std::endl;
+ });
+ tickTimer.start(200ms);
+
+ QTimer dbusTimer;
+ QObject::connect(&dbusTimer, &QTimer::timeout, &app, dbusWorker);
+ dbusTimer.start(2s);
+
+ return app.exec();
+}
--- /dev/null
+add_executable(future-example main.cpp)
+target_link_libraries(future-example
+ QCoro${QT_VERSION_MAJOR}Core
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Concurrent
+)
+set_target_defaults(future-example)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorofuture.h"
+
+#include <QCoreApplication>
+#include <QTimer>
+#include <QtConcurrent>
+
+#include <iostream>
+#include <random>
+
+QCoro::Task<> startTask() {
+ const auto data = co_await QtConcurrent::run([]() {
+ QVector<std::uint64_t> data;
+ std::random_device rd{};
+ std::mt19937 gen{rd()};
+ data.reserve(10'000'000);
+ for (int i = 0; i < 10'000'000; ++i) {
+ data.push_back(gen());
+ }
+ return data;
+ });
+
+ std::cout << "Generated " << data.size() << " random numbers" << std::endl;
+
+ const auto sum = co_await QtConcurrent::filteredReduced<std::uint64_t>(
+ data, [](const auto &) { return true; },
+ [](std::uint64_t &interm, std::uint64_t val) { interm += val; },
+ QtConcurrent::UnorderedReduce);
+
+ std::cout << "Calculated result: " << sum << std::endl;
+ qApp->quit();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app(argc, argv);
+ QTimer::singleShot(0, startTask);
+ return app.exec();
+}
--- /dev/null
+add_executable(iodevice-example main.cpp)
+target_link_libraries(iodevice-example
+ QCoro${QT_VERSION_MAJOR}Core
+ QCoro${QT_VERSION_MAJOR}Network
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Network
+)
+set_target_defaults(iodevice-example)
+
--- /dev/null
+#include "qcoroiodevice.h"
+#include "qcorotask.h"
+
+#include <QCoreApplication>
+#include <QHostAddress>
+#include <QTcpServer>
+#include <QTcpSocket>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+
+using namespace std::chrono_literals;
+
+class Server : public QObject {
+ Q_OBJECT
+public:
+ explicit Server(QHostAddress addr, uint16_t port) {
+ mServer.listen(addr, port);
+ connect(&mServer, &QTcpServer::newConnection, this, &Server::handleConnection);
+ }
+
+private Q_SLOTS:
+ QCoro::Task<> handleConnection() {
+ auto socket = mServer.nextPendingConnection();
+ while (socket->isOpen()) {
+ const auto data = co_await socket;
+ socket->write("PONG: " + data);
+ }
+ }
+
+private:
+ QTcpServer mServer;
+};
+
+class Client : public QObject {
+ Q_OBJECT
+public:
+ explicit Client(QHostAddress addr, uint16_t port) {
+ mSocket.connectToHost(addr, port);
+ connect(&mTimer, &QTimer::timeout, this, &Client::sendPing);
+ mTimer.start(300ms);
+ }
+
+private Q_SLOTS:
+ QCoro::Task<> sendPing() {
+ std::cout << "Sending ping..." << std::endl;
+ mSocket.write(QByteArray("PING #") + QByteArray::number(++mPing));
+ const auto response = co_await mSocket;
+ std::cout << "Received pong: " << response.constData() << std::endl;
+ }
+
+private:
+ int mPing = 0;
+ QTcpSocket mSocket;
+ QTimer mTimer;
+};
+
+int main(int argc, char **argv) {
+ QCoreApplication app{argc, argv};
+ Server server{QHostAddress::LocalHost, 6666};
+ Client client{QHostAddress::LocalHost, 6666};
+ return app.exec();
+}
+
+#include "main.moc"
--- /dev/null
+add_executable(network-example main.cpp)
+target_link_libraries(network-example
+ QCoro${QT_VERSION_MAJOR}Network
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Widgets
+ Qt${QT_VERSION_MAJOR}::Network
+)
+set_target_defaults(network-example)
+
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoro/network/qcoronetworkreply.h"
+#include "qcoro/qcorotask.h"
+
+#include <QApplication>
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion" // QSizePolicy seems problematic...
+#endif // __clang__
+#include <QBoxLayout>
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif // __clang__
+#include <QMainWindow>
+#include <QMessageBox>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QProgressBar>
+#include <QPushButton>
+
+#include <memory>
+
+// Programming langauges
+static const QUrl wikiUrl =
+ QUrl{QStringLiteral("https://www.wikidata.org/wiki/Special:EntityData/Q9143.json")};
+
+class MainWindow : public QMainWindow {
+ Q_OBJECT
+public:
+ MainWindow() {
+ mPb = new QProgressBar();
+ mPb->setVisible(false);
+ mPb->setMinimumWidth(200);
+ mPb->setMinimum(0);
+ mPb->setMaximum(0);
+
+ mBtn = new QPushButton(tr("Start Download"));
+ connect(mBtn, &QPushButton::clicked, this, &MainWindow::start);
+
+ auto *vbox = new QVBoxLayout();
+ vbox->addStretch(1);
+ vbox->addWidget(mPb);
+ vbox->addWidget(mBtn);
+ vbox->addStretch(1);
+
+ auto *hbox = new QHBoxLayout();
+ hbox->addStretch(1);
+ hbox->addLayout(vbox);
+ hbox->addStretch(1);
+
+ QWidget *w = new QWidget;
+ w->setLayout(hbox);
+ setCentralWidget(w);
+ }
+
+private Q_SLOTS:
+ QCoro::Task<> start() {
+ mPb->setVisible(true);
+ mBtn->setEnabled(false);
+ mBtn->setText(tr("Downloading ..."));
+
+ std::unique_ptr<QNetworkReply> reply(co_await mNam.get(QNetworkRequest{wikiUrl}));
+ if (reply->error()) {
+ QMessageBox::warning(
+ this, tr("Network request error"),
+ tr("Error occured during network request. Error code: %1").arg(reply->error()));
+ co_return;
+ }
+
+ mPb->setVisible(false);
+ mBtn->setEnabled(true);
+ mBtn->setText(tr("Done, download again"));
+ }
+
+private:
+ QNetworkAccessManager mNam;
+ QPushButton *mBtn = {};
+ QProgressBar *mPb = {};
+};
+
+int main(int argc, char **argv) {
+ QApplication app(argc, argv);
+ MainWindow window;
+ window.showNormal();
+ return app.exec();
+}
+
+#include "main.moc"
--- /dev/null
+add_executable(timer-example main.cpp)
+target_link_libraries(timer-example
+ QCoro${QT_VERSION_MAJOR}Core
+ Qt${QT_VERSION_MAJOR}::Core
+)
+set_target_defaults(timer-example)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorotask.h"
+#include "qcorotimer.h"
+
+#include <QCoreApplication>
+#include <QDateTime>
+#include <QTimer>
+
+#include <chrono>
+#include <iostream>
+
+using namespace std::chrono_literals;
+
+QCoro::Task<> runMainTimer() {
+ std::cout << "runMainTimer started" << std::endl;
+ QTimer timer;
+ timer.setInterval(2s);
+ timer.start();
+
+ std::cout << "Waiting for main timer..." << std::endl;
+ co_await timer;
+ std::cout << "Main timer ticked!" << std::endl;
+
+ qApp->quit();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app{argc, argv};
+ QTimer ticker;
+ QObject::connect(&ticker, &QTimer::timeout, &app, []() {
+ std::cout << QDateTime::currentDateTime().toString(Qt::ISODateWithMs).toStdString()
+ << " Secondary timer tick!" << std::endl;
+ });
+ ticker.start(200ms);
+
+ QTimer::singleShot(0, runMainTimer);
+ return app.exec();
+}
--- /dev/null
+site_name: QCoro
+site_description: QCoro is a C++ framework for using coroutines with Qt
+site_author: Daniel Vrátil
+site_url: https://qcoro.dev/
+copyright: Copyright © <a href="https://www.dvratil.cz">Daniel Vrátil</a>, all contents published under <a href="https://www.gnu.org/licenses/fdl-1.3.html">GNU FDL 1.3</a>, unless stated otherwise.
+repo_url: https://github.com/qcoro/qcoro
+repo_name: 'QCoro on GitHub'
+edit_uri: 'https://github.com/qcoro/qcoro/edit/main/docs/'
+
+theme:
+ name: material
+ custom_dir: docs/overrides
+ logo: assets/qcoro.svg
+ icon:
+ repo:
+ fontawesome/brands/github
+ features:
+ - navigation.expand
+ - navigation.tracking
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.indexes
+
+ palette:
+ # Palette toggle for automatic mode
+ - media: "(prefers-color-scheme: normal)"
+ primary: teal
+ accent: green
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: teal
+ accent: green
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: teal
+ accent: green
+ toggle:
+ icon: material/brightness-4
+ name: Switch to system preference
+
+markdown_extensions:
+ - pymdownx.highlight
+ - pymdownx.superfences
+ - pymdownx.inlinehilite
+ - pymdownx.extra
+ - admonition
+
+plugins:
+ - search:
+ separator: '[\s\-]+|::'
+ - include-markdown
+ - blogging:
+ dirs:
+ - news
+ sort:
+ from: new
+ by: creation
+ time_format: "%B %d, %Y"
+ meta_time_format: "%Y-%m-%d"
+ locale: en
+ theme:
+ name: button
+ options:
+ plain_button: true
+ - rss:
+ match_path: news/.*
+ date_from_meta:
+ as_creation: date
+ categories:
+ - categories
+ - tags
+
+ - macros:
+ module_name: docs/macros
+ - privacy
+
+extra_css:
+ - stylesheets/doctable.css
+
+extra:
+ social:
+ - icon: fontawesome/brands/twitter
+ link: https://twitter.com/danvratil
+ - icon: fontawesome/brands/mastodon
+ link: https://fosstodon.org/@danvratil
+ - icon: fontawesome/brands/github
+ link: https://github.com/danvratil
+
+nav:
+ - Home: index.md
+ - Building and Using QCoro: building-and-using.md
+ - Coroutines:
+ - Qt vs. co_await: coroutines/qt-vs-coawait.md
+ - co_await Explained: coroutines/coawait.md
+ - Further Reading: coroutines/reading.md
+ - News: news.md
+ - Reference:
+ - Coro:
+ - reference/coro/index.md
+ - QCoro::Task<T>: reference/coro/task.md
+ - QCoro::LazyTask<T>: reference/coro/lazytask.md
+ - QCoro::coro(): reference/coro/coro.md
+ - QCoro::Generator<T>: reference/coro/generator.md
+ - QCoro::AsyncGenerator<T>: reference/coro/asyncgenerator.md
+ - Core:
+ - reference/core/index.md
+ - Qt Signals: reference/core/signals.md
+ - QFuture: reference/core/qfuture.md
+ - QIODevice: reference/core/qiodevice.md
+ - QProcess: reference/core/qprocess.md
+ - QThread: reference/core/qthread.md
+ - QTimer: reference/core/qtimer.md
+ - Network:
+ - reference/network/index.md
+ - QAbstractSocket: reference/network/qabstractsocket.md
+ - QLocalSocket: reference/network/qlocalsocket.md
+ - QNetworkReply: reference/network/qnetworkreply.md
+ - QTcpServer: reference/network/qtcpserver.md
+ - DBus:
+ - reference/dbus/index.md
+ - QDBusPendingCall: reference/dbus/qdbuspendingcall.md
+ - QDBusPendingReply: reference/dbus/qdbuspendingreply.md
+ - WebSockets:
+ - reference/websockets/index.md
+ - QWebSocket: reference/websockets/qwebsocket.md
+ - QWebSocketServer: reference/websockets/qwebsocketserver.md
+ - Quick:
+ - reference/quick/index.md
+ - QCoro::ImageProvider: reference/quick/imageprovider.md
+ - Qml:
+ - reference/qml/index.md
+ - QCoro::QmlTask: reference/qml/qmltask.md
+ - Test:
+ - reference/test/index.md
+ - Changelog: changelog.md
+ - About:
+ - License: about/license.md
--- /dev/null
+# SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+# Custom includes
+include(GenerateHeaders)
+include(AddQCoroLibrary)
+
+include(CMakePackageConfigHelpers)
+
+add_qcoro_library(
+ NAME Coro
+ INTERFACE
+ NO_CMAKE_CONFIG
+ INCLUDEDIR Coro
+ CAMELCASE_HEADERS
+ QCoro
+ QCoroAsyncGenerator
+ QCoroFwd
+ QCoroGenerator
+ QCoroLazyTask
+ QCoroTask
+ HEADERS
+ concepts_p.h
+ coroutine.h
+ macros_p.h
+ waitoperationbase_p.h
+ impl/connect.h
+ impl/lazytask.h
+ impl/task.h
+ impl/taskawaiterbase.h
+ impl/taskbase.h
+ impl/taskfinalsuspend.h
+ impl/taskpromise.h
+ impl/taskpromisebase.h
+ impl/waitfor.h
+ QT_LINK_LIBRARIES
+ INTERFACE Core
+)
+
+if (NOT QCORO_DISABLE_DEPRECATED_TASK_H)
+ # Install Task conditionally
+ generate_headers(
+ task_cc_HEADERS
+ HEADER_NAMES Task
+ OUTPUT_DIR QCoro
+ ORIGINAL_HEADERS_VAR task_lc_HEADERS
+ )
+
+ install(
+ FILES ${task_lc_HEADERS}
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/qcoro/
+ COMPONENT Devel
+ )
+ install(
+ FILES ${task_cc_HEADERS}
+ DESTINATION ${QCORO_INSTALL_INCLUDEDIR}/QCoro/
+ COMPONENT Devel
+ )
+endif()
+
+
+# The QCoroCoroConfig includes the QCoroMacros.cmake so we can't
+# use the standard file generated by add_qcoro_library.
+configure_package_config_file(
+ "${CMAKE_CURRENT_SOURCE_DIR}/QCoroCoroConfig.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}CoroConfig.cmake"
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/QCoro${QT_VERSION_MAJOR}Coro
+ PATH_VARS CMAKE_INSTALL_INCLUDEDIR
+)
+
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/QCoro${QT_VERSION_MAJOR}CoroConfigVersion.cmake"
+ VERSION ${qcoro_VERSION}
+ COMPATIBILITY SameMajorVersion
+)
+
+# Install the extra macros file
+install(
+ FILES "QCoroMacros.cmake"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${QCORO_TARGET_PREFIX}Coro/"
+)
+
+############################
+
+add_subdirectory(core)
+
+if (QCORO_WITH_QTDBUS)
+ add_subdirectory(dbus)
+endif()
+
+if (QCORO_WITH_QTNETWORK)
+ add_subdirectory(network)
+endif()
+
+if (QCORO_WITH_QTWEBSOCKETS)
+ add_subdirectory(websockets)
+endif()
+
+if (QCORO_WITH_QTQUICK)
+ add_subdirectory(quick)
+endif()
+
+if (QCORO_WITH_QML)
+ add_subdirectory(qml)
+endif()
+
+if (QCORO_WITH_QTTEST)
+ add_subdirectory(test)
+endif()
--- /dev/null
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+find_dependency(Qt@QT_VERSION_MAJOR@Core)
+
+include("${CMAKE_CURRENT_LIST_DIR}/QCoro@QT_VERSION_MAJOR@CoroTargets.cmake")
+# Custom macros
+include("${CMAKE_CURRENT_LIST_DIR}/QCoroMacros.cmake")
+
+# Versionless target, for compatiblity with Qt6
+if (TARGET QCoro@QT_VERSION_MAJOR@::Coro AND NOT TARGET QCoro::Coro)
+ add_library(QCoro::Coro INTERFACE IMPORTED)
+ set_target_properties(QCoro::Coro PROPERTIES
+ INTERFACE_LINK_LIBRARIES "QCoro@QT_VERSION_MAJOR@::Coro"
+ )
+endif()
--- /dev/null
+macro(qcoro_enable_coroutines)
+ if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines")
+ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
+ if (MSVC)
+ # clang-cl behaves like MSVC and enables coroutines automatically when C++20 is enabled
+ else()
+ if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "16")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines-ts")
+ endif()
+ endif()
+ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ # MSVC auto-enables coroutine support when C++20 is enabled
+ else()
+ message(FATAL_ERROR "Compiler ${CMAKE_CXX_COMPILER_ID} is not currently supported.")
+ endif()
+endmacro()
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifndef Q_MOC_RUN
+
+#include <concepts>
+
+#if defined(__clang__)
+
+// Sadly, libc++ doesn't currently implement any concepts, so
+// we need to implement them ourselves.
+
+namespace QCoro::concepts {
+
+template<typename T>
+concept destructible = std::is_nothrow_destructible_v<T>;
+
+template<typename T, typename ... Args>
+concept constructible_from = destructible<T>
+ && std::is_constructible_v<T, Args...>;
+
+} // namespace QCoro::concepts
+
+#else
+
+namespace QCoro::concepts {
+
+using namespace std;
+
+} // namespace QCoro::concepts
+
+#endif // clang
+
+#endif // Q_MOC_RUN
--- /dev/null
+# SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME Core
+ INCLUDEDIR Core
+ SOURCES
+ qcoroiodevice.cpp
+ qcoroiodevice_p.cpp
+ qcoroprocess.cpp
+ qcorothread.cpp
+ qcorotimer.cpp
+ CAMELCASE_HEADERS
+ QCoroCore
+ QCoroIODevice
+ QCoroProcess
+ QCoroSignal
+ QCoroThread
+ QCoroTimer
+ QCoroFuture
+ HEADERS
+ impl/isqprivatesignal.h
+ QT_LINK_LIBRARIES
+ PUBLIC Core
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro
+)
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <string_view>
+#include <type_traits>
+
+#if defined(__cpp_lib_source_location)
+#include <source_location>
+#endif
+
+namespace QCoro::detail {
+/**
+ * A whacky helper to detect whether T is a QPrivateSignal type.
+ *
+ * The problem with QPrivateSignal is that it's a struct that's private
+ * to each QObject-derived class, so we can't simply do std::same_as_v<T, Obj::QPrivateSignal>
+ * because we cannot access the private QPrivateSignal struct from here.
+ *
+ * The only solution I could come up with abuses std::source_location
+ * to get a string with name of the function which also shows what T is,
+ * and then check whether the string contains "::QPrivateSignal" substring
+ * in the right place. The whole check is compile-time, so there's no runtime
+ * overhead.
+ *
+ * Unfortunately the output format of std::source_location::function_name() is
+ * implementation-specific, so we need to handle each compiler separately. This
+ * can cause truble in the future if an implementation changes the output format.
+ **/
+template<typename T>
+struct is_qprivatesignal {
+private:
+ static constexpr std::string_view qprivatesignal = "QPrivateSignal";
+
+ static constexpr auto functionName() noexcept {
+#if defined(_MSC_VER)
+ // While MSVC does support std::source_location, std::source_location::function_name()
+ // returns only the name of the function ("functionName"), but we need the fully qualified
+ // name, which MSVC-specific __FUNCSIG__ macro gives us
+ return __FUNCSIG__;
+#elif defined(__cpp_lib_source_location)
+ return std::source_location::current().function_name();
+#else
+ return __PRETTY_FUNCTION__;
+#endif
+ }
+
+ static constexpr bool getValue() {
+ // Clang: static auto QCoro::detail::is_qprivatesignal<Foo>::functionName() [T = Foo]
+ // GCC : static consteval auto QCoro::detail::is_qprivatesignal<T>::functionName() [with T = Foo]
+ // MSVC : const char *__cdecl QCoro::detail::is_qprivatesignal<Foo>::functionName(void)
+
+ // Note: can't use auto here as it gets deduced as const char* for some reason despite
+ // functionName() explicitly returning std::string_view.
+ constexpr std::string_view name{functionName()};
+#if defined(_MSC_VER)
+ constexpr auto end_pos = name.rfind('>');
+#else
+ constexpr auto end_pos = name.rfind(']');
+#endif
+
+ if ((end_pos == std::string_view::npos) || (end_pos < qprivatesignal.size())) {
+ return false;
+ }
+
+ for (auto pos = 1U; pos <= qprivatesignal.size(); ++pos) {
+ if (name[end_pos - pos] != qprivatesignal[qprivatesignal.size() - pos]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+public:
+ static constexpr bool value = getValue();
+};
+
+template<typename T>
+constexpr bool is_qprivatesignal_v = is_qprivatesignal<std::remove_cvref_t<T>>::value;
+
+} // namespace QCoro::detail
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroiodevice.h"
+#include "qcoroprocess.h"
+#include "qcorosignal.h"
+#include "qcorotimer.h"
+#include "qcorofuture.h"
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "macros_p.h"
+
+#include <type_traits>
+
+#include <QFuture>
+#include <QFutureWatcher>
+
+/*! \cond internal */
+
+namespace QCoro::detail {
+
+template<typename T>
+class QCoroFuture final {
+private:
+ template<typename T_>
+ class WaitForFinishedOperationBase {
+ public:
+ explicit WaitForFinishedOperationBase(const QFuture<T_> &future)
+ : mFuture(future) {}
+ Q_DISABLE_COPY(WaitForFinishedOperationBase)
+ QCORO_DEFAULT_MOVE(WaitForFinishedOperationBase)
+
+ bool await_ready() const noexcept {
+ return mFuture.isFinished() || mFuture.isCanceled();
+ }
+
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) {
+ auto *watcher = new QFutureWatcher<T_>();
+ QObject::connect(watcher, &QFutureWatcherBase::finished, [watcher, awaitingCoroutine]() mutable {
+ watcher->deleteLater();
+ awaitingCoroutine.resume();
+ });
+ watcher->setFuture(mFuture);
+ }
+
+ protected:
+ QFuture<T_> mFuture;
+ };
+
+ class WaitForFinishedOperationImplT : public WaitForFinishedOperationBase<T> {
+ public:
+ using WaitForFinishedOperationBase<T>::WaitForFinishedOperationBase;
+
+ T await_resume() const {
+ return this->mFuture.result();
+ }
+ };
+
+ class WaitForFinishedOperationImplVoid : public WaitForFinishedOperationBase<void> {
+ public:
+ using WaitForFinishedOperationBase<void>::WaitForFinishedOperationBase;
+
+ void await_resume() {
+ // This won't block, since we know for sure that the QFuture is already finished.
+ // The weird side-effect of this function is that it will re-throw the stored
+ // exception.
+ this->mFuture.waitForFinished();
+ }
+ };
+
+ using WaitForFinishedOperation = std::conditional_t<
+ std::is_void_v<T>, WaitForFinishedOperationImplVoid, WaitForFinishedOperationImplT>;
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ template<typename T_ = T> requires (!std::is_void_v<T_>)
+ class TakeResultOperation : public WaitForFinishedOperationBase<T_> {
+ public:
+ using WaitForFinishedOperationBase<T>::WaitForFinishedOperationBase;
+
+ T_ await_resume() {
+ return this->mFuture.takeResult();
+ }
+ };
+
+#endif
+
+
+ friend struct awaiter_type<QFuture<T>>;
+
+ QFuture<T> mFuture;
+
+public:
+ explicit QCoroFuture(const QFuture<T> &future)
+ : mFuture(future) {}
+
+ /*!
+ * \brief Equivalent to using `QCoroFuture::result()`.
+ *
+ * This function is provided for backwards API compatibility, new code should use
+ * `QCoroFuture::result()` instead.
+ *
+ * \see QCoroFuture<T>::result()
+ */
+ Task<T> waitForFinished() {
+ co_return co_await WaitForFinishedOperation{mFuture};
+ }
+
+ /*!
+ * \brief Asynchronously waits for the future to finish and returns the result.
+ *
+ * This is equivalent to using `QFutureWatcher` to wait for the future to finish and
+ * then obtainign the result using `QFuture::result()`.
+ *
+ * \see QCoroFuture<T>::takeResult()
+ */
+ Task<T> result() {
+ co_return co_await WaitForFinishedOperation{mFuture};
+ }
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ /*!
+ * \brief Asynchronously waits for the future to finish and takes (moves) the result from the future object.
+ *
+ * This is useful when you want to move the result from the future object into a local variable without
+ * copying it or when working with move-only types.
+ *
+ * Using this is equivalent to using `QFutureWatcher` to wait for the future to finish and then
+ * obtaining the result using `QFuture::takeResult()`.
+ *
+ * \see QCoroFuture<T>::result()
+ */
+ Task<T> takeResult() requires (!std::is_void_v<T>) {
+ co_return std::move(co_await TakeResultOperation<T>{mFuture});
+ }
+#endif
+};
+
+template<typename T>
+struct awaiter_type<QFuture<T>> {
+ using type = typename QCoroFuture<T>::WaitForFinishedOperation;
+};
+
+} // namespace QCoro::detail
+
+/*! \endcond */
+
+//! Returns a coroutine-friendly wrapper for QFuture object.
+/*!
+ * Returns a wrapper for the QFuture \c f that provides coroutine-friendly
+ * way to co_await the completion of the future.
+ *
+ * @see docs/reference/qfuture.md
+ */
+template<typename T>
+inline auto qCoro(const QFuture<T> &f) noexcept {
+ return QCoro::detail::QCoroFuture<T>{f};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroiodevice.h"
+#include "qcoroiodevice_p.h"
+#include "qcorosignal.h"
+
+#include <QByteArray>
+#include <QIODevice>
+#include <QTimer>
+
+using namespace QCoro::detail;
+
+QCoroIODevice::OperationBase::OperationBase(QIODevice *device)
+ : mDevice(device)
+{}
+
+void QCoroIODevice::OperationBase::finish(std::coroutine_handle<> awaitingCoroutine) {
+ QObject::disconnect(mConn);
+ QObject::disconnect(mCloseConn);
+ // Delayed trigger
+ QTimer::singleShot(0, [awaitingCoroutine]() mutable { awaitingCoroutine.resume(); });
+}
+
+QCoroIODevice::ReadOperation::ReadOperation(QIODevice *device, std::function<QByteArray(QIODevice *)> &&resultCb)
+ : OperationBase(device), mResultCb(std::move(resultCb)) {}
+
+bool QCoroIODevice::ReadOperation::await_ready() const noexcept {
+ return !mDevice || !mDevice->isOpen() || !mDevice->isReadable() ||
+ mDevice->bytesAvailable() > 0;
+}
+
+void QCoroIODevice::ReadOperation::await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ Q_ASSERT(mDevice);
+ mConn = QObject::connect(mDevice, &QIODevice::readyRead,
+ std::bind(&ReadOperation::finish, this, awaitingCoroutine));
+ mCloseConn =
+ QObject::connect(mDevice, &QIODevice::aboutToClose,
+ std::bind(&ReadOperation::finish, this, awaitingCoroutine));
+}
+
+QByteArray QCoroIODevice::ReadOperation::await_resume() {
+ return mResultCb(mDevice);
+}
+
+QCoroIODevice::ReadAllOperation::ReadAllOperation(QIODevice *device)
+ : ReadOperation(device, [](QIODevice *d) { return d->readAll(); }) {}
+
+QCoroIODevice::ReadAllOperation::ReadAllOperation(QIODevice &device)
+ : ReadAllOperation(&device) {}
+
+QCoroIODevice::QCoroIODevice(QIODevice *device)
+ : mDevice{device}
+{}
+
+QCoro::Task<QByteArray> QCoroIODevice::readAll(std::chrono::milliseconds timeout) {
+ const auto device = mDevice;
+ if (!co_await waitForReadyRead(timeout)) {
+ co_return QByteArray{};
+ }
+
+ co_return device->readAll();
+}
+
+QCoro::Task<QByteArray> QCoroIODevice::read(qint64 maxSize, std::chrono::milliseconds timeout) {
+ const auto device = mDevice;
+ if (!co_await waitForReadyRead(timeout)) {
+ co_return QByteArray{};
+ }
+
+ co_return device->read(maxSize);
+}
+
+QCoro::Task<QByteArray> QCoroIODevice::readLine(qint64 maxSize, std::chrono::milliseconds timeout) {
+ const auto device = mDevice;
+ if (!co_await waitForReadyRead(timeout)) {
+ co_return QByteArray{};
+ }
+
+ co_return device->readLine(maxSize);
+}
+
+QCoro::Task<qint64> QCoroIODevice::write(const QByteArray &buffer) {
+ const auto bytesWritten = mDevice->write(buffer);
+ qint64 bytesConfirmed = 0;
+ while (bytesConfirmed < bytesWritten) {
+ const auto flushed = co_await waitForBytesWritten(-1);
+ if (!flushed.has_value()) {
+ // There was an intermediate error and we don't know how much was actually
+ // written, so we report only what we know for sure was written.
+ break;
+ }
+ bytesConfirmed += *flushed;
+ }
+
+ co_return bytesConfirmed;
+}
+
+QCoro::Task<bool> QCoroIODevice::waitForReadyRead(int timeout_msecs) {
+ return waitForReadyRead(std::chrono::milliseconds(timeout_msecs));
+}
+
+QCoro::Task<bool> QCoroIODevice::waitForReadyRead(std::chrono::milliseconds timeout) {
+ if (!mDevice->isReadable()) {
+ co_return false;
+ }
+ if (mDevice->bytesAvailable() > 0) {
+ co_return true;
+ }
+
+ const auto result = co_await waitForReadyReadImpl(timeout);
+ co_return result.has_value();
+}
+
+QCoro::Task<std::optional<qint64>> QCoroIODevice::waitForBytesWritten(int timeout_msecs) {
+ return waitForBytesWritten(std::chrono::milliseconds(timeout_msecs));
+}
+
+QCoro::Task<std::optional<qint64>> QCoroIODevice::waitForBytesWritten(std::chrono::milliseconds timeout) {
+ if (!mDevice->isWritable()) {
+ co_return std::nullopt;
+ }
+ if (mDevice->bytesToWrite() == 0) {
+ co_return 0;
+ }
+
+ const auto result = co_await waitForBytesWrittenImpl(timeout);
+ co_return result;
+}
+
+QCoro::Task<std::optional<bool>> QCoroIODevice::waitForReadyReadImpl(std::chrono::milliseconds timeout) {
+ WaitSignalHelper helper(mDevice.data(), &QIODevice::readyRead);
+ co_return co_await qCoro(&helper, qOverload<bool>(&WaitSignalHelper::ready), timeout);
+}
+
+QCoro::Task<std::optional<qint64>> QCoroIODevice::waitForBytesWrittenImpl(std::chrono::milliseconds timeout) {
+ WaitSignalHelper helper(mDevice.data(), &QIODevice::bytesWritten);
+ co_return co_await qCoro(&helper, qOverload<qint64>(&WaitSignalHelper::ready), timeout);
+}
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "coroutine.h"
+#include "macros_p.h"
+#include "waitoperationbase_p.h"
+#include "qcorocore_export.h"
+
+#include <QPointer>
+
+class QIODevice;
+
+/*! \cond internal */
+
+namespace QCoro::detail {
+
+class QCOROCORE_EXPORT QCoroIODevice {
+private:
+ class OperationBase {
+ public:
+ Q_DISABLE_COPY(OperationBase)
+ QCORO_DEFAULT_MOVE(OperationBase)
+
+ virtual ~OperationBase() = default;
+
+ protected:
+ explicit OperationBase(QIODevice *device);
+
+ virtual void finish(std::coroutine_handle<> awaitingCoroutine);
+
+ QPointer<QIODevice> mDevice;
+ QMetaObject::Connection mConn;
+ QMetaObject::Connection mCloseConn;
+ QMetaObject::Connection mFinishedConn;
+ };
+
+protected:
+ class ReadOperation : public OperationBase {
+ public:
+ ReadOperation(QIODevice *device, std::function<QByteArray(QIODevice *)> &&resultCb);
+ Q_DISABLE_COPY(ReadOperation)
+ QCORO_DEFAULT_MOVE(ReadOperation)
+
+ virtual bool await_ready() const noexcept;
+ virtual void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept;
+ QByteArray await_resume();
+
+ private:
+ std::function<QByteArray(QIODevice *)> mResultCb;
+ };
+
+ class ReadAllOperation final : public ReadOperation {
+ public:
+ explicit ReadAllOperation(QIODevice *device);
+ explicit ReadAllOperation(QIODevice &device);
+ };
+
+ template<typename T>
+ friend struct awaiter_type;
+public:
+ //! Constructor.
+ explicit QCoroIODevice(QIODevice *device);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::readAll()`][qdoc-qiodevice-readall].
+ *
+ * Waits until the `QIODevice` emits [`readyRead()`][qdoc-qiodevice-readyRead] and
+ * then calls `readAll()`.
+ *
+ * Optional parameter timeout specifies how long to wait for the operation to
+ * complete. If timeout occurs before any data are available for reading, the
+ * operation will return an empty QByteArray. If the \c timeout is -1, the operation
+ * will never time out.
+ *
+ * Identical to asynchronously calling
+ * ```cpp
+ * device.waitForReadyRead(timeout);
+ * device.readAll();
+ * ```
+ *
+ * [qdoc-qiodevice-readall]: https://doc.qt.io/qt-5/qiodevice.html#readAll
+ * [qdoc-qiodevice-readyRead]: https://doc.qt.io/qt-5/qiodevice.html#readyRead
+ */
+ Task<QByteArray> readAll(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::read()`][qdoc-qiodevice-read].
+ *
+ * Waits until the `QIODevice` emits [`readyRead()`][qdoc-qiodevice-readyRead] and
+ * then calls [`read()`][qdoc-qiodevice-read] to read up to \c maxSize bytes.
+ *
+ * Optional parameter timeout specifies how long to wait for the operation to
+ * complete. If timeout occurs before any data are available for reading, the
+ * operation will return an empty QByteArray. If the timeout is -1, the operation
+ * will never time out.
+ *
+ * Identical to asynchronously calling
+ * ```cpp
+ * device.waitForReadyRead(timeout);
+ * device.read();
+ * ```
+ *
+ * [qdoc-qiodevice-read]: https://doc.qt.io/qt-5/qiodevice.html#read-1
+ * [qdoc-qiodevice-readyRead]: https://doc.qt.io/qt-5/qiodevice.html#readyRead
+ */
+ Task<QByteArray> read(qint64 maxSize,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::readLine()`][qdoc-qiodevice-readLine].
+ *
+ * Waits until the `QIODevice` emits [`readyRead()`][qdoc-qiodevice-readyRead] and
+ * then calls [`readLine()`][qdoc-qiodevice-readLIne] to read until the end-of-line
+ * character is reached or up to \c maxSize characters are read.
+ *
+ * Optional parameter timeout specifies how long to wait for the operation to
+ * complete. If timeout occurs before any data are available for reading, the
+ * operation will return an empty QByteArray. If the \c timeout is -1, the operation
+ * will never time out.
+ *
+ * Identical to asynchronously calling
+ * ```cpp
+ * device.waitForReadyRead();
+ * device.readLine();
+ * ```
+ *
+ * [qdoc-qiodevice-readLine]: https://doc.qt.io/qt-5/qiodevice.html#readLine
+ * [qdoc-qiodevice-readyRead]: https://doc.qt.io/qt-5/qiodevice.html#readyRead
+ */
+ Task<QByteArray> readLine(qint64 maxSize = 0,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ // TODO
+ //auto bytesAvailable(qint64 minBytes) {
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::write`][qdoc-qiodevice-write].
+ *
+ * Returns immediately if the `QIODevice` is unbuffered, blocks until the `QIODevice`
+ * emits [`bytesWritten()`][qdoc-qiodevice-bytesWritten] signal with total bytes equal
+ * to the size of the input \c buffer.
+ *
+ * Identical to asynchronously calling
+ * ```cpp
+ * device.write(data);
+ * device.waitForBytesWritten();
+ * ```
+ *
+ * [qdoc-qiodevice-write]: https://doc.qt.io/qt-5/qiodevice.html#write-2
+ * [qdoc-qiodevice-bytesWritten]: https://doc.qt.io/qt-5/qiodevice.html#bytesWritten
+ */
+ Task<qint64> write(const QByteArray &buffer);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::waitForReadyRead`][qdoc-qiodevice-waitForReadyRead].
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1, the operation will enver time out.
+ *
+ * [qdoc-qiodevice-waitForReadyRead]: https://doc.qt.io/qt-5/qiodevice.html#waitForReadyRead
+ */
+ Task<bool> waitForReadyRead(std::chrono::milliseconds timeout);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::waitForReadyRead`][qdoc-qiodevice-waitForReadyRead].
+ *
+ * If the \c timeout_msecs is -1, the operation will never time out.
+ *
+ * [qdoc-qiodevice-waitForReadyRead]: https://doc.qt.io/qt-5/qiodevice.html#waitForReadyRead
+ */
+ Task<bool> waitForReadyRead(int timeout_msecs);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::waitForBytesWritten`][qdoc-qiodevice-waitForBytesWritten].
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1, the operation will never time out.
+ *
+ * [qdoc-qiodevice-waitForBytesWritten]: https://doc.qt.io/qt-5/qiodevice.html#waitForBytesWritten
+ */
+ Task<std::optional<qint64>> waitForBytesWritten(std::chrono::milliseconds timeout);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QIODevice::waitForBytesWritten`][qdoc-qiodevice-waitForBytesWritten].
+ *
+ * If the \c timeout_msecs is -1, the operation will never time out.
+ *
+ * [qdoc-qiodevice-waitForBytesWritten]: https://doc.qt.io/qt-5/qiodevice.html#waitForBytesWritten
+ */
+ Task<std::optional<qint64>> waitForBytesWritten(int timeout_msecs);
+
+protected:
+ virtual Task<std::optional<bool>> waitForReadyReadImpl(std::chrono::milliseconds timeout);
+ virtual Task<std::optional<qint64>> waitForBytesWrittenImpl(std::chrono::milliseconds timeout);
+
+ QPointer<QIODevice> mDevice = {};
+};
+
+template<typename T> requires std::is_base_of_v<QIODevice, T>
+struct awaiter_type<T> {
+ using type = QCoroIODevice::ReadAllOperation;
+};
+template<typename T> requires std::is_base_of_v<QIODevice, T>
+struct awaiter_type<T *> {
+ using type = QCoroIODevice::ReadAllOperation;
+};
+
+} // namespace QCoro::detail
+
+/*! \endcond */
+
+//! Returns a coroutine-friendly wrapper for a QIODevice-derived object.
+/*!
+ * Returns a wrapper for QIODevice \c d that provides coroutine-friendly way
+ * of co_awaiting reading and writing operation.
+ *
+ * @see docs/reference/qiodevice.md
+ */
+inline auto qCoro(QIODevice &d) noexcept {
+ return QCoro::detail::QCoroIODevice{&d};
+}
+//! \copydoc qCoro(QIODevice *d) noexcept
+inline auto qCoro(QIODevice *d) noexcept {
+ return QCoro::detail::QCoroIODevice{d};
+}
+
+// If you got here due to a compile error, make sure to #include the QCoro header
+// for the corresponding class, so that the qCoro() overload that wraps the QIODevice-derived
+// classes in their respective QCoroIODevice-derived wrappers is used.
+//
+// Wrapping those classes directly into QCoroIODevice will cause co_awaiting certain operations to not
+// work as expected.
+class QAbstractSocket;
+auto qCoro(QAbstractSocket *) noexcept; // You are likely missing "#include <QCoroAbstractSocket>"
+class QLocalSocket;
+auto qCoro(QLocalSocket *) noexcept; // You are likely missing "#include <QCoroLocalSocket>"
+class QNetworkReply;
+auto qCoro(QNetworkReply *) noexcept; // You are likely missing "#include <QNetworkReply>"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroiodevice_p.h"
+
+using namespace QCoro::detail;
+
+WaitSignalHelper::WaitSignalHelper(const QIODevice *device, void(QIODevice::*signalFunc)())
+ : QObject()
+ , mReady(connect(device, signalFunc, this, [this]() { this->emitReady(true); }))
+ , mAboutToClose(connect(device, &QIODevice::aboutToClose, this, [this]() { this->emitReady(false); }))
+{}
+
+WaitSignalHelper::WaitSignalHelper(const QIODevice *device, void(QIODevice::*signalFunc)(qint64))
+ : QObject()
+ , mReady(connect(device, signalFunc, this, &WaitSignalHelper::emitReady<qint64>))
+ , mAboutToClose(connect(device, &QIODevice::aboutToClose, this, [this]() { this->emitReady(static_cast<qint64>(0)); }))
+{}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QIODevice>
+#include "qcorocore_export.h"
+
+namespace QCoro::detail {
+
+class QCOROCORE_EXPORT WaitSignalHelper : public QObject {
+ Q_OBJECT
+public:
+ explicit WaitSignalHelper(const QIODevice *device, void(QIODevice::*signalFunc)());
+ explicit WaitSignalHelper(const QIODevice *device, void(QIODevice::*signalFunc)(qint64));
+
+Q_SIGNALS:
+ void ready(bool result);
+ void ready(qint64 result);
+
+protected:
+ template<typename T>
+ void emitReady(T result) {
+ cleanup();
+ Q_EMIT this->ready(result);
+ }
+
+ virtual void cleanup() {
+ disconnect(mReady);
+ disconnect(mAboutToClose);
+ }
+
+ QMetaObject::Connection mReady;
+ QMetaObject::Connection mAboutToClose;
+};
+
+} // namespace QCoro::detail
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include <QtGlobal>
+
+#if QT_CONFIG(process)
+
+#include "qcoroprocess.h"
+#include "qcorosignal.h"
+
+#include <QProcess>
+
+using namespace QCoro::detail;
+
+QCoroProcess::QCoroProcess(QProcess *process)
+ : QCoroIODevice(process)
+{}
+
+QCoro::Task<bool> QCoroProcess::waitForStarted(int timeout_msecs) {
+ return waitForStarted(std::chrono::milliseconds{timeout_msecs});
+}
+
+QCoro::Task<bool> QCoroProcess::waitForStarted(std::chrono::milliseconds timeout) {
+ const auto *process = qobject_cast<QProcess *>(mDevice.data());
+ if (process->state() == QProcess::Starting) {
+ const auto started = co_await qCoro(process, &QProcess::started, timeout);
+ co_return started.has_value();
+ }
+
+ co_return process->state() == QProcess::Running;
+}
+
+QCoro::Task<bool> QCoroProcess::waitForFinished(int timeout_msecs) {
+ return waitForFinished(std::chrono::milliseconds{timeout_msecs});
+}
+
+QCoro::Task<bool> QCoroProcess::waitForFinished(std::chrono::milliseconds timeout) {
+ const auto *process = qobject_cast<QProcess *>(mDevice.data());
+ if (process->state() == QProcess::NotRunning) {
+ co_return false;
+ }
+
+ const auto finished = co_await qCoro(process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), timeout);
+ co_return finished.has_value();
+}
+
+QCoro::Task<bool> QCoroProcess::start(QIODevice::OpenMode mode, std::chrono::milliseconds timeout) {
+ static_cast<QProcess *>(mDevice.data())->start(mode);
+ return waitForStarted(timeout);
+}
+
+QCoro::Task<bool> QCoroProcess::start(const QString &program, const QStringList &arguments,
+ QIODevice::OpenMode mode, std::chrono::milliseconds timeout) {
+ static_cast<QProcess *>(mDevice.data())->start(program, arguments, mode);
+ return waitForStarted(timeout);
+}
+
+#endif // QT_CONFIG(process)
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "waitoperationbase_p.h"
+#include "qcoroiodevice.h"
+#include "qcorocore_export.h"
+
+#include <chrono>
+
+#include <QIODevice>
+
+#if QT_CONFIG(process)
+
+class QProcess;
+
+namespace QCoro::detail {
+
+using namespace std::chrono_literals;
+
+//! QProcess wrapper with co_awaitable-friendly API.
+class QCOROCORE_EXPORT QCoroProcess : public QCoroIODevice {
+public:
+ explicit QCoroProcess(QProcess *process);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted].
+ *
+ * Returns true if the process has started successfully, otherwise returns false (if the
+ * operation timed out or if an error occured).
+ *
+ * If \c timeout_msecs is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForStarted]: https://doc.qt.io/qt-5/qprocess.html#waitForStarted
+ */
+ Task<bool> waitForStarted(int timeout_msecs = 30'000);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted].
+ *
+ * Returns true if the process has started successfully, otherwise returns false (if the
+ * operation timed out or if an error occured).
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForStarted]: https://doc.qt.io/qt-5/qprocess.html#waitForStarted
+ */
+ Task<bool> waitForStarted(std::chrono::milliseconds timeout);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QProcess::waitForFinished()`][qtdoc-qprocess-waitForFinished].
+ *
+ * Returns true if the process has finished, otherwise returns false (if the operation timed
+ * out, if an error occured or if this `QProcess` is already finished.
+ *
+ * If \c timeout_msecs is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForFinished]: https://doc.qt.io/qt-5/qprocess.html#waitForFinished
+ */
+ Task<bool> waitForFinished(int timeout_msecs = 30'000);
+
+ /*!
+ * \brief Co_awaitable equivalent to [`QProcess::waitForFinished()`][qtdoc-qprocess-waitForFinished].
+ *
+ * Returns true if the process has finished, otherwise returns false (if the operation timed
+ * out, if an error occured or if this `QProcess` is already finished.
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForFinished]: https://doc.qt.io/qt-4/qprocess.html#waitForFinished
+ */
+ Task<bool> waitForFinished(std::chrono::milliseconds timeout);
+
+ /*!
+ * \brief Executes a new process and waits for it to start
+ *
+ * Co_awaitable equivalent to calling [`QProcess::start()`][qtdoc-qprocess-start-2]
+ * followed by [`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted].
+ *
+ * Returns true if the process has started successfully, otherwise returns false (if the
+ * operation timed out or if an error occurred).
+ *
+ * If the \c timeout is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForStarted]: https://doc.qt.io/qt-5/qprocess.html#waitForStarted
+ * [qtdoc-qprocess-start-2]: https://doc.qt.io/qt-5/qprocess.html#start-2
+ */
+ Task<bool> start(QIODevice::OpenMode mode = QIODevice::ReadWrite,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+
+ /*!
+ * \brief Executes a new process and waits for it to start
+ *
+ * Co_awaitable equivalent to calling [`QProcess::start()`][qtdoc-qprocess-start]
+ * followed by [`QProcess::waitForStarted()`][qtdoc-qprocess-waitForStarted].
+ *
+ * Returns true if the process has started successfully, otherwise returns false (if the
+ * operation timed out or if an error occurred).
+ *
+ * If the \c timeout is -1 the operation will never time out.
+ *
+ * [qtdoc-qprocess-waitForStarted]: https://doc.qt.io/qt-5/qprocess.html#waitForStarted
+ * [qtdoc-qprocess-start]: https://doc.qt.io/qt-5/qprocess.html#start-2
+ */
+ Task<bool> start(const QString &program, const QStringList &arguments,
+ QIODevice::OpenMode mode = QIODevice::ReadWrite,
+ std::chrono::milliseconds timeout = std::chrono::seconds(30));
+};
+
+} // namespace QCoro::detail
+
+//! Returns a coroutine-friendly wrapper for QProcess object.
+/*!
+ * Returns a wrapper for the QProcess \c p that provides coroutine-friendly
+ * way to co_await the process to start or finish.
+ *
+ * @see docs/reference/qprocess.md
+ */
+inline auto qCoro(QProcess &p) noexcept {
+ return QCoro::detail::QCoroProcess{&p};
+}
+//! \copydoc qCoro(QProcess &p) noexcept
+inline auto qCoro(QProcess *p) noexcept {
+ return QCoro::detail::QCoroProcess{p};
+}
+
+#endif // QT_CONFIG(process)
--- /dev/null
+// SPDX-FileCopyrightText: 2021-2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "coroutine.h"
+#include "macros_p.h"
+#include "concepts_p.h"
+#include "qcorotask.h"
+#include "qcoroasyncgenerator.h"
+
+#include <QObject>
+#include <QPointer>
+#include <QTimer>
+
+#include <cassert>
+#include <optional>
+#include <deque>
+#include <tuple>
+#include <type_traits>
+
+#include "impl/isqprivatesignal.h"
+
+namespace QCoro::detail {
+
+namespace concepts {
+
+//! Simplistic QObject concept.
+template<typename T>
+concept QObject = requires(T *obj) {
+ requires std::is_base_of_v<::QObject, T>;
+ requires std::is_same_v<decltype(T::staticMetaObject), const QMetaObject>;
+};
+
+} // namespace concepts
+
+template<concepts::QObject T, typename FuncPtr>
+class QCoroSignalBase {
+private:
+ template<typename T1, typename T2>
+ struct concat_tuple;
+
+ template<typename Arg, typename... TupleTypes>
+ struct concat_tuple<Arg, std::tuple<TupleTypes...>> {
+ using type = std::tuple<TupleTypes..., Arg>;
+ };
+
+ template<typename Tuple, typename... Args>
+ struct filtered_tuple;
+
+ template<typename Tuple, typename Arg, typename... Args>
+ struct filtered_tuple<Tuple, Arg, Args...> {
+ using type = std::conditional_t<
+ is_qprivatesignal_v<Arg>, typename filtered_tuple<Tuple, Args...>::type,
+ typename filtered_tuple<typename concat_tuple<Arg, Tuple>::type, Args...>::type>;
+ };
+
+ template<typename Tuple>
+ struct filtered_tuple<Tuple> {
+ using type = Tuple;
+ };
+
+ template<class>
+ struct args_tuple;
+
+ template<class R, class... Args>
+ struct args_tuple<R(Args...)> {
+ using type = std::tuple<std::remove_cvref_t<Args>...>;
+ };
+
+ template<class R, class Obj, class... Args>
+ struct args_tuple<R (Obj::*)(Args...)> {
+ using type = typename filtered_tuple<std::tuple<>, std::remove_cvref_t<Args>...>::type;
+ };
+
+ template<typename Arg>
+ struct result_type_from_tuple;
+
+ template<typename Arg>
+ struct result_type_from_tuple<std::tuple<Arg>> {
+ using type = Arg;
+ };
+
+ template<typename... Args>
+ struct result_type_from_tuple<std::tuple<Args...>> {
+ using type = std::tuple<Args...>;
+ };
+
+ using result_tuple = typename args_tuple<std::remove_cvref_t<FuncPtr>>::type;
+
+public:
+ /**!
+ * The result_type is std::optional of
+ * * T if result_tuple is std::tuple<T>
+ * * result_tuple otherwise
+ **/
+ using result_type = std::optional<typename result_type_from_tuple<result_tuple>::type>;
+
+ QCoroSignalBase(const QCoroSignalBase &) = delete;
+ QCoroSignalBase &operator=(const QCoroSignalBase &) = delete;
+ QCoroSignalBase(QCoroSignalBase &&) noexcept = default;
+ QCoroSignalBase &operator=(QCoroSignalBase &&) noexcept = default;
+
+ ~QCoroSignalBase() {
+ if (static_cast<bool>(mConn)) {
+ QObject::disconnect(mConn);
+ }
+ }
+
+ void handleTimeout(std::coroutine_handle<> awaitingCoroutine) {
+ if (mTimeoutTimer) {
+ QObject::connect(mTimeoutTimer.get(), &QTimer::timeout, mObj,
+ [this, awaitingCoroutine]() {
+ QObject::disconnect(mConn);
+ awaitingCoroutine.resume();
+ }, Qt::DirectConnection); // force coro to be resumed on our thread, not mObj's thread
+ mTimeoutTimer->start();
+ }
+ }
+
+protected:
+ QCoroSignalBase(T *obj, FuncPtr &&funcPtr, std::chrono::milliseconds timeout)
+ : mObj(obj), mFuncPtr(std::forward<FuncPtr>(funcPtr)) {
+ if (timeout.count() > -1) {
+ mTimeoutTimer = std::make_unique<QTimer>();
+ mTimeoutTimer->setInterval(timeout);
+ mTimeoutTimer->setSingleShot(true);
+ }
+ }
+
+ template<typename... Args>
+ struct select_last {
+ using type = typename decltype((std::type_identity<Args>{}, ...))::type;
+ };
+
+ template<typename StoreResultCb, typename... Args>
+ constexpr void storeResult(StoreResultCb &&storeResult, Args &&...args) {
+ using LastArg = typename select_last<Args...>::type;
+ if constexpr (is_qprivatesignal_v<LastArg>) {
+ // Based on https://stackoverflow.com/a/77026174/4601437
+ // Remove the last element (which is a QPrivateSignal) from the tuple
+ auto all = std::forward_as_tuple(std::forward<Args>(args)...);
+ auto reduced = [&]<std::size_t... I>(std::index_sequence<I...>) constexpr {
+ return std::make_tuple(std::get<I>(all)...);
+ }(std::make_index_sequence<sizeof...(Args) - 1>{});
+ // Use the shortened tuple as arguments to mResult.emplace()
+ std::apply(std::forward<StoreResultCb>(storeResult), std::move(reduced));
+ } else {
+ std::invoke(std::forward<StoreResultCb>(storeResult), std::forward<Args>(args)...);
+ }
+ }
+
+ template<typename StoreResultCb>
+ constexpr void storeResult(StoreResultCb &&storeResult) {
+ std::invoke(std::forward<StoreResultCb>(storeResult));
+ }
+
+protected:
+ QPointer<T> mObj;
+ FuncPtr mFuncPtr;
+ QMetaObject::Connection mConn;
+ std::unique_ptr<QTimer> mTimeoutTimer;
+};
+
+template<concepts::QObject T, typename FuncPtr>
+class QCoroSignal : public QCoroSignalBase<T, FuncPtr> {
+public:
+ using typename QCoroSignalBase<T, FuncPtr>::result_type;
+
+ QCoroSignal(T *obj, FuncPtr &&ptr, std::chrono::milliseconds timeout)
+ : QCoroSignalBase<T, FuncPtr>(obj, std::forward<FuncPtr>(ptr), timeout)
+ , mDummyReceiver(std::make_unique<QObject>()) {}
+ QCoroSignal(const QCoroSignal &) = delete;
+ QCoroSignal(QCoroSignal &&other) noexcept
+ : QCoroSignalBase<T, FuncPtr>(std::move(other))
+ , mResult(std::move(other.mResult))
+ , mDummyReceiver(std::move(other.mDummyReceiver)) {
+ if (this->mConn) {
+ QObject::disconnect(this->mConn);
+ setupConnection();
+ }
+ }
+
+ QCoroSignal &operator=(QCoroSignal &&other) noexcept {
+ QCoroSignalBase<T, FuncPtr>::operator=(std::move(other));
+ std::swap(mResult, other.mResult);
+ std::swap(mDummyReceiver, other.mDummyReceiver);
+ if (this->mConn) {
+ QObject::disconnect(this->mConn);
+ setupConnection();
+ }
+ return *this;
+ }
+
+ QCoroSignal &operator=(const QCoroSignal &) = delete;
+ ~QCoroSignal() = default;
+
+
+ bool await_ready() const noexcept {
+ return this->mObj.isNull();
+ }
+
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ this->handleTimeout(awaitingCoroutine);
+ mAwaitingCoroutine = awaitingCoroutine;
+ setupConnection();
+ }
+
+ result_type await_resume() {
+ return std::move(mResult);
+ }
+
+private:
+ void setupConnection() {
+ Q_ASSERT(!this->mConn);
+ this->mConn = QObject::connect(
+ this->mObj, this->mFuncPtr, this->mDummyReceiver.get(),
+ [this](auto &&...args) mutable {
+ if (this->mTimeoutTimer) {
+ this->mTimeoutTimer->stop();
+ }
+ QObject::disconnect(this->mConn);
+
+ this->storeResult([this](auto && ...args) {
+ mResult.emplace(std::forward<decltype(args)>(args)...);
+ }, std::forward<decltype(args)>(args)...);
+
+ if (mAwaitingCoroutine) {
+ mAwaitingCoroutine.resume();
+ }
+ },
+ Qt::QueuedConnection);
+ }
+
+ result_type mResult;
+ std::coroutine_handle<> mAwaitingCoroutine;
+ std::unique_ptr<QObject> mDummyReceiver;
+};
+
+template<concepts::QObject T, typename FuncPtr>
+QCoroSignal(T *, FuncPtr &&, std::chrono::milliseconds) -> QCoroSignal<T, FuncPtr>;
+
+template<concepts::QObject T, typename FuncPtr>
+class QCoroSignalQueue : public QCoroSignalBase<T, FuncPtr> {
+public:
+ using typename QCoroSignalBase<T, FuncPtr>::result_type;
+
+ QCoroSignalQueue(T *obj, FuncPtr &&ptr, std::chrono::milliseconds timeout)
+ : QCoroSignalBase<T, FuncPtr>(obj, std::forward<FuncPtr>(ptr), timeout) {
+ setupConnection();
+ }
+
+ QCoroSignalQueue(QCoroSignalQueue &&) = delete;
+ QCoroSignalQueue(const QCoroSignalQueue &) = delete;
+ QCoroSignalQueue &operator=(QCoroSignalQueue &&) = delete;
+ QCoroSignalQueue &operator=(const QCoroSignalQueue &) = delete;
+ ~QCoroSignalQueue() = default;
+
+ auto operator co_await() noexcept {
+ struct Awaiter {
+ explicit Awaiter(QCoroSignalQueue &queue)
+ : mQueue(queue)
+ {}
+
+ bool await_ready() const noexcept {
+ return !mQueue.isValid() || !mQueue.empty();
+ }
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ mQueue.handleTimeout(awaitingCoroutine);
+ mQueue.setAwaiter(awaitingCoroutine);
+ }
+ result_type await_resume() {
+ return mQueue.dequeue();
+ }
+
+ private:
+ QCoro::detail::QCoroSignalQueue<T, FuncPtr> &mQueue;
+ };
+ return Awaiter{*this};
+ }
+
+ bool isValid() const {
+ return !this->mObj.isNull();
+ }
+
+ bool empty() const {
+ return mQueue.empty();
+ }
+
+ result_type dequeue() {
+ if (mQueue.empty()) {
+ return std::nullopt;
+ }
+
+ auto result = std::move(mQueue.front());
+ mQueue.pop_front();
+ return result;
+ }
+
+ void setAwaiter(std::coroutine_handle<> awaiter) {
+ mAwaitingCoroutine = awaiter;
+ }
+
+private:
+ void setupConnection() {
+ if (this->mConn) {
+ return;
+ }
+ this->mConn = QObject::connect(
+ this->mObj, this->mFuncPtr, &this->mDummyReceiver,
+ [this](auto && ...args) mutable {
+ if (this->mTimeoutTimer) {
+ this->mTimeoutTimer->stop();
+ }
+
+ this->storeResult([this](auto && ...args) {
+ mQueue.emplace_back(std::forward<decltype(args)>(args)...);
+ }, std::forward<decltype(args)>(args) ...);
+
+ if (mAwaitingCoroutine) {
+ mAwaitingCoroutine.resume();
+ }
+ }, Qt::QueuedConnection);
+
+ }
+
+ std::coroutine_handle<> mAwaitingCoroutine;
+ std::deque<typename result_type::value_type> mQueue;
+ QObject mDummyReceiver;
+};
+
+template<concepts::QObject T, typename FuncPtr>
+QCoroSignalQueue(T *, FuncPtr &&, std::chrono::milliseconds) -> QCoroSignalQueue<T, FuncPtr>;
+
+
+} // namespace QCoro::detail
+
+//! Allows co_awaiting on signal emission with a timeout.
+/*!
+ * Returns an Awaitable object that allows co_awaiting for a singal
+ * to be emitted within the specified timeout. If the signal has exactly
+ * one argument, then the value of the argument is returned as a result
+ * of awaiting the coroutine. If the signal has two or more arguments,
+ * then the arguments are returned as a tuple. If the signal has no
+ * arguments, then the result of the coroutine is an empty tuple.
+ *
+ * If the timeout occurs before the signal is emitted, the result of the
+ * coroutine is an empty optional. If the \c timeout is -1 the operation
+ * will never time out.
+ */
+template<QCoro::detail::concepts::QObject T, typename FuncPtr>
+inline auto qCoro(T *obj, FuncPtr &&ptr, std::chrono::milliseconds timeout)
+ -> QCoro::Task<typename QCoro::detail::QCoroSignal<T, FuncPtr>::result_type> {
+ auto result = co_await QCoro::detail::QCoroSignal(obj, std::forward<FuncPtr>(ptr), timeout);
+ co_return std::move(result);
+}
+
+//! Allows co_awaiting on signal emission.
+/*!
+ * Returns an Awaitable object that allows co_awaiting for a signal to
+ * be emitted. If the signal has exactly one argument, then the value
+ * of the argument is returned as a result of awaiting the coroutine.
+ * If the signal has two or more arguments, then the arguments are
+ * returned as a tuple. If the signal has no arguments, then the result
+ * of the coroutine is an empty tuple.
+ *
+ * @see docs/reference/coro.md
+ */
+template<QCoro::detail::concepts::QObject T, typename FuncPtr>
+inline auto qCoro(T *obj, FuncPtr &&ptr)
+ -> QCoro::Task<typename QCoro::detail::QCoroSignal<T, FuncPtr>::result_type::value_type> {
+ auto result = co_await qCoro<T, FuncPtr>(obj, std::forward<FuncPtr>(ptr), std::chrono::milliseconds{-1});
+ co_return std::move(*result);
+}
+
+template<QCoro::detail::concepts::QObject T, typename FuncPtr>
+inline auto qCoroSignalListener(T *obj, FuncPtr &&ptr,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds{-1})
+ -> QCoro::AsyncGenerator<typename QCoro::detail::QCoroSignalQueue<T, FuncPtr>::result_type::value_type> {
+
+ using SignalQueue = QCoro::detail::QCoroSignalQueue<T, FuncPtr>;
+
+ // The actual generator is in a wrapper function, so that we can perform
+ // some initialization (constructing signalQueue) in the qCoroSignalListener()
+ // function before the generator gets initially suspended.
+ constexpr auto innerGenerator = [](std::unique_ptr<SignalQueue> signalQueue) ->
+ QCoro::AsyncGenerator<typename SignalQueue::result_type::value_type> {
+ Q_FOREVER {
+ auto result = co_await *signalQueue;
+ if (!result.has_value()) { // timeout
+ break;
+ }
+
+ co_yield std::move(*result);
+ }
+ };
+
+ return innerGenerator(std::make_unique<SignalQueue>(obj, std::forward<FuncPtr>(ptr), timeout));
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorothread.h"
+#include "qcorosignal.h"
+
+#include <QThread>
+#include <QEvent>
+#include <QCoreApplication>
+
+using namespace QCoro;
+using namespace QCoro::detail;
+
+namespace QCoro::detail {
+
+class ContextHelper : public QObject {
+ Q_OBJECT
+public:
+ static QEvent::Type eventType;
+
+ explicit ContextHelper(std::coroutine_handle<> awaiter, QThread *thread)
+ : mThread(thread)
+ , mAwaiter(awaiter)
+ {}
+
+ bool event(QEvent *event) override {
+ if (event->type() == eventType) {
+ Q_ASSERT(QThread::currentThread() == mThread);
+ mAwaiter.resume();
+ return true;
+ }
+
+ return QObject::event(event);
+ }
+
+private:
+ QThread *mThread;
+ std::coroutine_handle<> mAwaiter;
+};
+
+QEvent::Type ContextHelper::eventType = static_cast<QEvent::Type>(QEvent::registerEventType());
+
+class ThreadContextPrivate {
+public:
+ explicit ThreadContextPrivate(QThread *thread)
+ : mThread(thread)
+ {}
+
+ QThread *mThread;
+ std::unique_ptr<ContextHelper> mContext;
+};
+
+} // namespace QCoro::detail
+
+ThreadContext::ThreadContext(QThread *thread)
+ : d(std::make_unique<ThreadContextPrivate>(thread))
+{}
+
+#ifdef Q_CC_GNU
+ThreadContext::ThreadContext(ThreadContext &&) noexcept = default;
+#endif
+
+ThreadContext::~ThreadContext() = default;
+
+bool ThreadContext::await_ready() const noexcept {
+ return false; // never ready!
+}
+
+void ThreadContext::await_suspend(std::coroutine_handle<> awaiter) noexcept {
+ d->mContext = std::make_unique<ContextHelper>(awaiter, d->mThread);
+ d->mContext->moveToThread(d->mThread);
+ qCoro(d->mThread).waitForStarted().then([this]() {
+ auto *event = new QEvent(static_cast<QEvent::Type>(detail::ContextHelper::eventType));
+ QCoreApplication::postEvent(d->mContext.get(), event);
+ });
+}
+
+void ThreadContext::await_resume() noexcept {}
+
+
+ThreadContext QCoro::moveToThread(QThread *thread) {
+ return ThreadContext(thread);
+}
+
+QCoroThread::QCoroThread(QThread *thread)
+ : mThread(thread)
+{}
+
+QCoro::Task<bool> QCoroThread::waitForStarted(std::chrono::milliseconds timeout) {
+ if (mThread->isRunning()) {
+ co_return true;
+ }
+ if (mThread->isFinished()) {
+ co_return false;
+ }
+
+ const auto result = co_await qCoro(mThread.data(), &QThread::started, timeout);
+ co_return result.has_value();
+}
+
+QCoro::Task<bool> QCoroThread::waitForFinished(std::chrono::milliseconds timeout) {
+ if (mThread->isFinished()) {
+ co_return true;
+ }
+ if (!mThread->isRunning()) {
+ co_return false;
+ }
+
+ const auto result = co_await qCoro(mThread.data(), &QThread::finished, timeout);
+ co_return result.has_value();
+}
+
+#include "qcorothread.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QPointer>
+
+#include "qcorocore_export.h"
+#include "qcoro/coroutine.h"
+
+#include <chrono>
+
+class QThread;
+
+namespace QCoro {
+template<typename T>
+class Task;
+
+namespace detail {
+class ThreadContextPrivate;
+} // namespace detail
+
+class ThreadContext {
+public:
+ explicit ThreadContext(QThread *thread);
+ ~ThreadContext();
+ ThreadContext(const ThreadContext &) = delete;
+ ThreadContext &operator=(const ThreadContext &) = delete;
+#ifdef Q_CC_GNU
+ // Workaround for the a GCC bug(?) where GCC tries to move the ThreadContext
+ // into QCoro::TaskPromise::await_transform() (most likely).
+ ThreadContext(ThreadContext &&) noexcept;
+#else
+ ThreadContext(ThreadContext &&) = delete;
+#endif
+ ThreadContext &operator=(ThreadContext &&) = delete;
+
+ bool await_ready() const noexcept;
+ void await_suspend(std::coroutine_handle<> awaiter) noexcept;
+ void await_resume() noexcept;
+
+private:
+ std::unique_ptr<detail::ThreadContextPrivate> d;
+};
+
+ThreadContext moveToThread(QThread *thread);
+
+} // namespace QCoro
+
+namespace QCoro::detail {
+
+class QCOROCORE_EXPORT QCoroThread {
+public:
+ explicit QCoroThread(QThread *thread);
+
+ /**
+ * \brief Coroutine that waits for a thread to get started.
+ *
+ * \return Returns `true` when the thread is already running or when it starts within the
+ * specified timeout. If the thread has already finished, or the operation times out, the
+ * coroutine returns `false`.
+ *
+ * If the timeout is -1 the operation will never time out.
+ *
+ * See [`QThread::started()`][qtdoc-qthread-started] documentation for details.
+ *
+ * [qtdoc-qthread-started]: https://doc.qt.io/qt-5/qthread.html#started
+ */
+ Task<bool> waitForStarted(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ /**
+ * \brief Coroutine that waits for a thread to finish.
+ *
+ * \return Returns `true` when the thread has already finished or when it finishes within
+ * specified timeout. If the thread is not running and hasn't finished yet, or when the
+ * operation times out, the coroutine returns `false`.
+ *
+ * If the timeout is -1 the operation will never time out.
+ *
+ * See [`QThread::finished()`][qtdoc-qthread-finished] documentation for details.
+ *
+ * [qdoc-qthread-finished]: https://doc.qt.io/qt-5/qthread.html#finished
+ */
+ Task<bool> waitForFinished(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+private:
+ QPointer<QThread> mThread;
+};
+
+} // namespace QCoro::detail
+
+inline auto qCoro(QThread *thread) {
+ return QCoro::detail::QCoroThread(thread);
+}
+
+inline auto qCoro(QThread &thread) {
+ return QCoro::detail::QCoroThread(&thread);
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorotimer.h"
+#include "qcorosignal.h"
+
+#include <QMetaObject>
+#include <QPointer>
+
+using namespace QCoro::detail;
+
+QCoroTimer::WaitForTimeoutOperation::WaitForTimeoutOperation(QTimer *timer)
+ : mTimer(timer) {}
+
+QCoroTimer::WaitForTimeoutOperation::WaitForTimeoutOperation(QTimer &timer)
+ : WaitForTimeoutOperation(&timer) {}
+
+bool QCoroTimer::WaitForTimeoutOperation::await_ready() const noexcept {
+ return !mTimer || !mTimer->isActive();
+}
+
+void QCoroTimer::WaitForTimeoutOperation::await_suspend(std::coroutine_handle<> awaitingCoroutine) {
+ if (mTimer && mTimer->isActive()) {
+ mConn = QObject::connect(mTimer, &QTimer::timeout, [this, awaitingCoroutine]() mutable {
+ QObject::disconnect(mConn);
+ awaitingCoroutine.resume();
+ });
+ } else {
+ awaitingCoroutine.resume();
+ }
+}
+
+void QCoroTimer::WaitForTimeoutOperation::await_resume() const {}
+
+QCoroTimer::QCoroTimer(QTimer *timer)
+ : mTimer(timer) {}
+
+QCoro::Task<> QCoroTimer::waitForTimeout() const {
+ if (mTimer->isActive()) {
+ co_await qCoro(mTimer.data(), &QTimer::timeout);
+ }
+}
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "qcorocore_export.h"
+
+#include <QMetaObject>
+#include <QPointer>
+#include <QTimer>
+
+/*! \cond internal */
+
+namespace QCoro::detail {
+
+class QCOROCORE_EXPORT QCoroTimer {
+private:
+ class WaitForTimeoutOperation {
+ public:
+ explicit WaitForTimeoutOperation(QTimer *timer);
+ explicit WaitForTimeoutOperation(QTimer &timer);
+
+ bool await_ready() const noexcept;
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine);
+ void await_resume() const;
+ private:
+ QMetaObject::Connection mConn;
+ QPointer<QTimer> mTimer;
+ };
+
+ friend struct awaiter_type<QTimer *>;
+ friend struct awaiter_type<QTimer>;
+
+ QPointer<QTimer> mTimer;
+public:
+ explicit QCoroTimer(QTimer *timer);
+
+ Task<void> waitForTimeout() const;
+};
+
+template<>
+struct awaiter_type<QTimer *> {
+ using type = QCoroTimer::WaitForTimeoutOperation;
+};
+
+template<>
+struct awaiter_type<QTimer> {
+ using type = QCoroTimer::WaitForTimeoutOperation;
+};
+
+} // namespace QCoro::detail
+
+namespace QCoro {
+
+//! A coroutine that suspends for given period of time.
+template<typename Rep, typename Period>
+QCoro::Task<> sleepFor(const std::chrono::duration<Rep, Period> &timeout) {
+ QTimer timer;
+ timer.setSingleShot(true);
+ timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(timeout));
+ co_await timer;
+}
+
+//! A coroutine that suspends until the specified time.
+template<typename Clock, typename Duration>
+QCoro::Task<> sleepUntil(const std::chrono::time_point<Clock, Duration> &when) {
+ const auto tp = when.time_since_epoch() - std::chrono::steady_clock::now().time_since_epoch();
+ return sleepFor(tp);
+}
+
+} // namespace QCoro
+
+/*! \endcond */
+
+//! Returns a coroutine-friendly wrapper for QTimer object.
+/*!
+ * Returns a wrapper for the QTimer \c timer that provides coroutine-friendly
+ * way to co_await the timeout.
+ *
+ * @see docs/reference/qtimer.md
+ */
+
+inline auto qCoro(QTimer *timer) noexcept {
+ return QCoro::detail::QCoroTimer{timer};
+}
+
+//! \copydoc qCoro(QTimer *)
+inline auto qCoro(QTimer &timer) noexcept{
+ return QCoro::detail::QCoroTimer{&timer};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <version>
+#include <utility>
+
+// __cpp_lib_coroutine is not defined if the compiler doesn't support coroutines
+// (__cpp_impl_coroutine), e.g. clang as of 13.0.
+#if defined(__cpp_lib_coroutine)
+#include <coroutine>
+#elif defined(__clang__)
+// Implement our own <coroutine> header in a way that is compatible with the standard.
+// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf
+
+#include <type_traits> // void_t
+#include <cstddef> // size_t
+
+// Intrinsincs for Clang
+// https://clang.llvm.org/docs/LanguageExtensions.html#c-coroutines-support-builtins
+extern "C" {
+void __builtin_coro_destroy(void *addr);
+void __builtin_coro_resume(void *addr);
+bool __builtin_coro_done(void *addr);
+void* __builtin_coro_promise(void *addr, int alignment, bool from_promise);
+void *__builtin_coro_noop();
+}
+
+// 17.12.1 Header <coroutine> synopsis
+namespace std {
+
+// 17.12.2, coroutine traits
+// (omitted, because we implement them in std::experimental namespace and import them into the std
+// namespace).
+// template<class R, class .. ArgTypes>
+// struct coroutine_traits;
+
+// 17.12.3, coroutine traits
+template<class Promise = void>
+struct coroutine_handle;
+
+// 17.12.3.6, comparison operators
+constexpr bool operator==(coroutine_handle<> x, coroutine_handle<> y) noexcept;
+// constexpr strong_ordering operator<=>(coroutine_handle<> x, coroutine_handle<> y) noexcept;
+
+// 17.12.3.7, hash support
+//template<class T> struct hash;
+//template<class P> struct hash<coroutine_handle<P>>;
+
+// 17.12.4, n-op- coroutines
+struct noop_coroutine_promise;
+
+template<>
+struct coroutine_handle<noop_coroutine_promise>;
+using noop_coroutine_handle = coroutine_handle<noop_coroutine_promise>;
+noop_coroutine_handle noop_coroutine() noexcept;
+
+// 17.12.5, trivial awaitables
+struct suspend_never;
+struct suspend_always;
+
+} // namespace std
+
+
+// Implementation
+namespace std {
+
+// Clang checks for std::experimental::coroutine_traits explicitly, so we must define the types
+// in the experimental namespace.
+namespace experimental {
+
+template<class R, class = void>
+struct __coroutine_traits_base {};
+
+template<class R>
+struct __coroutine_traits_base<R, void_t<typename R::promise_type>> {
+ using promise_type = typename R::promise_type;
+};
+
+
+// 17.12.2, coroutine traits
+
+template<class R, class ... ArgTypes>
+struct coroutine_traits : __coroutine_traits_base<R> {};
+
+
+// Clang requires that std::experimental::coroutine_handle is a class template
+template<typename Promise>
+struct coroutine_handle : public std::coroutine_handle<Promise> {};
+
+} // namespace experimental
+
+#if defined(__cpp_lib_coroutine)
+// Import std::experimental::coroutine_traits into the std namespace
+template<typename R, typename ... ArgTypes>
+using coroutine_traits = std::experimental::coroutine_traits<R, ArgTypes ...>;
+#endif
+
+// 17.12.3, coroutine handle
+
+template<>
+struct coroutine_handle<void> {
+ // 17.12.3.1, construct/reset
+ constexpr coroutine_handle() noexcept {}
+ constexpr coroutine_handle(nullptr_t) noexcept {}
+ coroutine_handle &operator=(nullptr_t) noexcept {
+ m_ptr = nullptr;
+ return *this;
+ }
+
+ // 17.12.3.2, export/import
+ constexpr void *address() const noexcept {
+ return m_ptr;
+ }
+
+ static constexpr coroutine_handle from_address(void *addr) noexcept {
+ coroutine_handle handle;
+ handle.m_ptr = addr;
+ return handle;
+ }
+
+ // 17.12.3.3, observers
+ constexpr explicit operator bool() const noexcept {
+ return m_ptr != nullptr;
+ }
+ bool done() const {
+ return __builtin_coro_done(m_ptr);
+ }
+
+ // 17.12.3.4, resumption
+ void operator()() const {
+ resume();
+ }
+ void resume() const {
+ __builtin_coro_resume(m_ptr);
+ }
+ void destroy() const {
+ __builtin_coro_destroy(m_ptr);
+ }
+
+protected:
+ void *m_ptr = nullptr;
+};
+
+template<class Promise>
+struct coroutine_handle : public coroutine_handle<> {
+ // 17.12.3.1, construct, reset
+ using coroutine_handle<>::coroutine_handle;
+ static coroutine_handle from_promise(Promise &promise) {
+ coroutine_handle handle;
+ handle.m_ptr = __builtin_coro_promise(&promise, alignof(Promise), /* from-promise=*/ true);
+ return handle;
+ }
+ coroutine_handle &operator=(nullptr_t) noexcept {
+ this->m_ptr = nullptr;
+ return *this;
+ }
+
+ // 17.12.3.2, export/import
+ static constexpr coroutine_handle from_address(void *addr) noexcept {
+ coroutine_handle handle;
+ handle.m_ptr = addr;
+ return handle;
+ }
+
+ //17.12.3.5, promise access
+ Promise &promise() const {
+ return *reinterpret_cast<Promise *>(
+ __builtin_coro_promise(m_ptr, alignof(Promise), /*from-promise=*/false));
+ }
+};
+
+// 17.12.3.6, comparison operators
+constexpr bool operator==(coroutine_handle<> x, coroutine_handle<> y) noexcept {
+ return x.address() == y.address();
+}
+
+//constexpr strong_ordering operator<=>(coroutine_handle<> x, coroutine_handle<> y) noexcept;
+
+// 17.12.4, no-op coroutines
+struct noop_coroutine_promise {};
+
+template<>
+struct coroutine_handle<noop_coroutine_promise>;
+using noop_coroutine_handle = coroutine_handle<noop_coroutine_promise>;
+
+template<>
+struct coroutine_handle<noop_coroutine_promise> : public coroutine_handle<> {
+ // 17.12.4.2.1, observers
+ constexpr explicit operator bool() const noexcept { return true; }
+ constexpr bool done() const noexcept { return false; }
+ constexpr void operator()() const noexcept {}
+ constexpr void resume() const noexcept {}
+ constexpr void destroy() const noexcept {}
+
+ noop_coroutine_promise &promise() const noexcept {
+ return *reinterpret_cast<noop_coroutine_promise *>(
+ __builtin_coro_promise(__builtin_coro_noop(),
+ alignof(noop_coroutine_promise), false));
+ }
+
+private:
+ coroutine_handle() noexcept
+ : coroutine_handle<>(from_address(__builtin_coro_noop())) {}
+
+ friend noop_coroutine_handle noop_coroutine() noexcept;
+};
+
+inline noop_coroutine_handle noop_coroutine() noexcept {
+ return {};
+}
+
+// 17.12.5, trivial awaitables
+
+struct suspend_never {
+ constexpr bool await_ready() const noexcept { return true; }
+ constexpr void await_resume() const noexcept {}
+ constexpr void await_suspend(coroutine_handle<>) const noexcept {}
+};
+
+struct suspend_always {
+ constexpr bool await_ready() const noexcept { return false; }
+ constexpr void await_suspend(coroutine_handle<>) const noexcept {}
+ constexpr void await_resume() const noexcept {}
+};
+
+} // namespace std
+
+#else // defined(__clang__)
+#pragma error "Current compiler does not support coroutines, or is not supported by QCoro."
+#endif // defined(__cpp_lib_coroutine)
+
+// The QCORO_STD macro is no longer needed (with the code above), but keep it for backwards
+// compatibility.
+#ifdef QCORO_NO_DEPRECATED_QCOROSTD
+#define QCORO_STD std
+#else // QCORO_NO_DEPRECATED_QCOROSTD
+#ifdef _MSC_VER
+ #define _QCORO_STRINGIFY2(x) #x
+ #define _QCORO_STRINGIFY(x) _QCORO_STRINGIFY2(x)
+ #define QCORO_STD \
+ __pragma(message(__FILE__ "(" _QCORO_STRINGIFY(__LINE__) ") QCORO_STD macro is deprecated, use regular 'std' namespace instead, or pass /DQCORO_NO_DEPRECATED_QCOROSTD to suppress this warning.")) \
+ std
+#else // GCC, clang
+ #define QCORO_STD \
+ _Pragma("GCC warning \"QCORO_STD macro is deprecated, use regular 'std' namespace instead, or pass -DQCORO_NO_DEPRECATED_QCOROSTD to suppress this warning.\"") \
+ std
+#endif // _MSC_VER
+#endif // QCORO_NO_DEPRECATED_QCOROSTD
+
+// Moc doesn't seem to understand something in the <concepts> header...
+#ifndef Q_MOC_RUN
+
+#include "concepts_p.h"
+
+namespace QCoro {
+
+namespace detail {
+
+
+template<typename T>
+concept has_await_methods = requires(T t) {
+ { t.await_ready() } -> std::same_as<bool>;
+ {t.await_suspend(std::declval<std::coroutine_handle<>>())};
+ {t.await_resume()};
+};
+
+template<typename T>
+concept has_member_operator_coawait = requires(T t) {
+ // TODO: Check that result of co_await() satisfies Awaitable again
+ { t.operator co_await() };
+};
+
+template<typename T>
+concept has_nonmember_operator_coawait = requires(T t) {
+ // TODO: Check that result of the operator satisfied Awaitable again
+#if defined(_MSC_VER) && !defined(__clang__)
+ // FIXME: MSVC is unable to perform ADL lookup for operator co_await and just fails to compile
+ { ::operator co_await(static_cast<T &&>(t)) };
+#else
+ { operator co_await(static_cast<T &&>(t)) };
+#endif
+};
+
+} // namespace detail
+
+//! A concept describing the Awaitable type
+/*!
+ * Awaitable type is a type that can be passed as an argument to co_await.
+ */
+template<typename T>
+concept Awaitable = detail::has_member_operator_coawait<T> ||
+ detail::has_nonmember_operator_coawait<T> ||
+ detail::has_await_methods<T>;
+
+} // namespace QCoro
+
+#endif // Q_MOC_RUN
--- /dev/null
+# SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME DBus
+ SOURCES
+ qcorodbuspendingcall.cpp
+ CAMELCASE_HEADERS
+ QCoroDBus
+ QCoroDBusPendingCall
+ QCoroDBusPendingReply
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro Core
+ QT_LINK_LIBRARIES
+ PUBLIC Core DBus
+)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorodbuspendingcall.h"
+#include "qcorodbuspendingreply.h"
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorodbuspendingcall.h"
+#include "qcorosignal.h"
+
+#include <QDBusPendingCall>
+
+using namespace QCoro::detail;
+
+QCoroDBusPendingCall::WaitForFinishedOperation::WaitForFinishedOperation(const QDBusPendingCall &call)
+ : mCall(call)
+{}
+
+bool QCoroDBusPendingCall::WaitForFinishedOperation::await_ready() const noexcept {
+ return mCall.isFinished();
+}
+
+void QCoroDBusPendingCall::WaitForFinishedOperation::await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+auto *watcher = new QDBusPendingCallWatcher{mCall};
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
+ [awaitingCoroutine](auto *watcher) mutable {
+ awaitingCoroutine.resume();
+ watcher->deleteLater();
+ });
+}
+
+QDBusMessage QCoroDBusPendingCall::WaitForFinishedOperation::await_resume() const {
+ Q_ASSERT(mCall.isFinished());
+ return mCall.reply();
+}
+
+QCoroDBusPendingCall::QCoroDBusPendingCall(const QDBusPendingCall &call)
+ : mCall(call)
+{}
+
+QCoro::Task<QDBusMessage> QCoroDBusPendingCall::waitForFinished() {
+ QDBusPendingCallWatcher watcher{mCall};
+ co_await qCoro(&watcher, &QDBusPendingCallWatcher::finished);
+ co_return watcher.reply();
+}
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "qcorodbus_export.h"
+
+#include <QDBusPendingCallWatcher>
+
+class QDBusMessage;
+class QDBusPendingCall;
+
+/*! \cond internal */
+
+namespace QCoro::detail {
+
+class QCORODBUS_EXPORT QCoroDBusPendingCall {
+private:
+ class WaitForFinishedOperation {
+ public:
+ explicit WaitForFinishedOperation(const QDBusPendingCall &call);
+
+ bool await_ready() const noexcept;
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept;
+ QDBusMessage await_resume() const;
+ private:
+ const QDBusPendingCall &mCall;
+ };
+
+ const QDBusPendingCall &mCall;
+
+ friend struct awaiter_type<QDBusPendingCall>;
+public:
+ //! Constructor.
+ explicit QCoroDBusPendingCall(const QDBusPendingCall &call);
+
+ /*!
+ \brief Operation that allows co_awaiting completion of the pending DBus call.
+
+ <!-- doc-waitForFinished-start -->
+ Waits until the DBus call is finished. This is equivalent to using
+ [`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher] and waiting for it
+ to emit the [`finished()`][qdoc-qdbuspendingcallwatcher-finished] signal.
+
+ Returns a `QDBusMessage` representing the reply to the call.
+
+ If the call is already finished or has an error, the coroutine will not suspend and the `co_await`
+ expression will return immediatelly.
+
+ It is also possible to just directly use a `QDBusPendingCall` in a `co_await`
+ expression to await its completion:
+ ```cpp
+ QDBusPendingCall pendingCall = interface.asyncCall(...);
+ const auto reply = co_await pendingCall;
+ ```
+
+ The above is equivalent to:
+ ```cpp
+ QDBusPendingCall pendingCall = interface.asyncCall(...);
+ const auto reply = co_await qCoro(pendingCall).waitForFinished();
+ ```
+
+ This is a coroutine-friendly equivalent to using [`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher]:
+
+ ```cpp
+ QDBusPendingCall call = interface.asyncCall(...);
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call);
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
+ this, [](QDBusPendingCallWatcher *watcher) {
+ watcher->deleteLater();
+ const QDBusReply<...> reply = *watcher;
+ ...
+ });
+ ```
+
+ [qdoc-qdbuspendingcallwatcher]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html
+ [qdoc-qdbuspendingcallwatcher-finished]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html#finished
+ <!-- doc-waitForFinished-end -->
+
+ @see docs/reference/qdbuspendingcall.md
+ */
+ Task<QDBusMessage> waitForFinished();
+};
+
+template<>
+struct awaiter_type<QDBusPendingCall> {
+ using type = QCoroDBusPendingCall::WaitForFinishedOperation;
+};
+
+} // namespace QCoro::detail
+
+/*! \endcond */
+
+//! Returns a co_await-friendly wrapper for QDBusPendingCall object
+/*!
+ * Returns a wrapper for QDBusPendingCall \c call that provides coroutine-friendly
+ * way to co_await completion of the call.
+ *
+ * @see docs/reference/qdbuspendingcall.md
+ */
+inline auto qCoro(const QDBusPendingCall &call) {
+ return QCoro::detail::QCoroDBusPendingCall{call};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "qcorosignal.h"
+
+#include <qglobal.h>
+#include <QDBusPendingReply>
+
+class QDBusMessage;
+
+/*! \cond internal */
+
+namespace QCoro::detail {
+
+template<typename ... Args>
+class QCoroDBusPendingReply {
+private:
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ // QDBusPendingReply is a variadic template since Qt6, but in Qt5 the
+ // maximum number of template arguments was 8, so we simulate the Qt5
+ // behavior here.
+ static_assert(sizeof...(Args) <= 8, "In Qt5 QDBusPendingReply has maximum 8 arguments.");
+#endif
+
+ class WaitForFinishedOperation {
+ public:
+ explicit WaitForFinishedOperation(const QDBusPendingReply<Args ...> &reply)
+ : mReply(reply)
+ {}
+
+ bool await_ready() const noexcept {
+ return mReply.isFinished();
+ }
+
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) {
+ auto *watcher = new QDBusPendingCallWatcher{mReply};
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
+ [awaitingCoroutine](auto *watcher) mutable {
+ awaitingCoroutine.resume();
+ watcher->deleteLater();
+ });
+ }
+
+ QDBusPendingReply<Args ...> await_resume() const {
+ Q_ASSERT(mReply.isFinished());
+ return mReply;
+ }
+
+ private:
+ QDBusPendingReply<Args ...> mReply;
+ };
+
+ QDBusPendingReply<Args ...> mReply;
+
+ friend struct awaiter_type<QDBusPendingReply<Args ...>>;
+public:
+ //! Constructor.
+ explicit QCoroDBusPendingReply(const QDBusPendingReply<Args ...> &reply)
+ : mReply(reply) {}
+
+ /*!
+ \brief Operation that allows co_awaiting completion of the pending DBus reply.
+
+ <!-- doc-waitForFinished-start -->
+ Waits until the DBus call is finished. This is equivalent to using
+ [`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher] and waiting for it
+ to emit the [`finished()`][qdoc-qdbuspendingcallwatcher-finished] signal.
+
+ Returns a `QDBusMessage` representing the received reply. If the reply is already
+ finished or an error has occurred the coroutine will not suspend and will return
+ a result immediatelly.
+
+ This is a coroutine-friendly equivalent to using [`QDBusPendingCallWatcher`][qdoc-qdbuspendingcallwatcher]:
+
+ ```cpp
+ QDBusPendingCall call = interface.asyncCall(...);
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call);
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
+ this, [](QDBusPendingCallWatcher *watcher) {
+ watcher->deleteLater();
+ const QDBusPendingReply<...> reply = *watcher;
+ ...
+ });
+ ```
+
+ It is also possible to just directly use a `QDBusPendingReply` in a `co_await`
+ expression to await its completion:
+ ```cpp
+ QDBusPendingReply<...> pendingReply = interface.asyncCall(...);
+ const auto reply = co_await pendingReply;
+ ```
+
+ The above is equivalent to:
+ ```cpp
+ QDBusPendingReply<...> pendingReply = interface.asyncCall(...);
+ const auto reply = co_await qCoro(pendingReply).waitForFinished();
+ ```
+
+ [qdoc-qdbuspendingcallwatcher]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html
+ [qdoc-qdbuspendingcallwatcher-finished]: https://doc.qt.io/qt-5/qdbuspendingcallwatcher.html#finished
+
+ <!-- doc-waitForFinished-end -->
+
+ @see docs/reference/qdbuspendingreply.md
+ */
+ Task<QDBusPendingReply<Args ...>> waitForFinished() {
+ if (!mReply.isFinished()) {
+ QDBusPendingCallWatcher watcher{mReply};
+ co_await qCoro(&watcher, &QDBusPendingCallWatcher::finished);
+ co_return watcher.reply();
+ }
+ co_return mReply;
+ }
+};
+
+template<typename ... Args>
+struct awaiter_type<QDBusPendingReply<Args ...>> {
+ using type = typename QCoroDBusPendingReply<Args ...>::WaitForFinishedOperation;
+};
+
+} // namespace QCoro::detail
+
+/*! \endcond */
+
+//! Returns a coroutine-friendly wrapper for a QDBusPendingReply object.
+/*!
+ * Returns a wrapper for the QDBusPendingReply \c reply that provides
+ * a coroutine-friendly way to await the completion of the pending reply.
+ *
+ * Note that is is also possible to just directly `co_await` the `QDBusPendingReply`
+ * completion without using the wrapper class:
+ *
+ * ```
+ * QDBusPendingReply<...> pendingReply = interface.asyncCall(...);
+ * const auto reply = co_await pendingReply;
+ * ```
+ *
+ * @see docs/reference/qdbuspendingreply.md
+ */
+template<typename ... Args>
+inline auto qCoro(const QDBusPendingReply<Args ...> &reply) {
+ return QCoro::detail::QCoroDBusPendingReply<Args ...>{reply};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+#include <QPointer>
+
+namespace QCoro
+{
+
+template <typename T, typename QObjectSubclass, typename Callback>
+requires std::is_invocable_v<Callback> || std::is_invocable_v<Callback, T> || std::is_invocable_v<Callback, QObjectSubclass *> || std::is_invocable_v<Callback, QObjectSubclass *, T>
+inline void connect(QCoro::Task<T> &&task, QObjectSubclass *context, Callback func) {
+ QPointer ctxWatcher = context;
+ if constexpr (std::is_same_v<T, void>) {
+ task.then([ctxWatcher, func = std::move(func)]() {
+ if (ctxWatcher) {
+ if constexpr (std::is_member_function_pointer_v<Callback>) {
+ (ctxWatcher->*func)();
+ } else {
+ func();
+ }
+ }
+ });
+ } else {
+ task.then([ctxWatcher, func = std::move(func)](auto &&value) {
+ if (ctxWatcher) {
+ if constexpr (std::is_invocable_v<Callback, QObjectSubclass, T>) {
+ (ctxWatcher->*func)(std::forward<decltype(value)>(value));
+ } else if constexpr (std::is_invocable_v<Callback, T>) {
+ func(std::forward<decltype(value)>(value));
+ } else {
+ Q_UNUSED(value);
+ if constexpr (std::is_member_function_pointer_v<Callback>) {
+ (ctxWatcher->*func)();
+ } else {
+ func();
+ }
+ }
+ }
+ });
+ }
+}
+
+template <typename T, typename QObjectSubclass, typename Callback>
+requires detail::TaskConvertible<T>
+ && (std::is_invocable_v<Callback> || std::is_invocable_v<Callback, detail::convertible_awaitable_return_type_t<T>> || std::is_invocable_v<Callback, QObjectSubclass *> || std::is_invocable_v<Callback, QObjectSubclass *, detail::convertible_awaitable_return_type_t<T>>)
+ && (!detail::isTask_v<T>)
+inline void connect(T &&future, QObjectSubclass *context, Callback func) {
+ auto task = detail::toTask(std::move(future));
+ connect(std::move(task), context, func);
+}
+
+} // namespace QCoro
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorolazytask.h"
+#include "qcorotask.h"
+
+#ifndef NDEBUG
+#include <QDebug>
+#endif
+
+namespace QCoro {
+
+namespace detail {
+
+template<typename T>
+inline LazyTask<T> LazyTaskPromise<T>::get_return_object() noexcept {
+ return LazyTask<T>(std::coroutine_handle<LazyTaskPromise>::from_promise(*this));
+}
+
+template<typename T>
+inline std::suspend_always LazyTaskPromise<T>::initial_suspend() const noexcept {
+ return {};
+}
+
+} // namespace detail
+
+template<typename T>
+inline LazyTask<T>::~LazyTask() {
+ #ifndef NDEBUG
+ if (this->mCoroutine && !this->mCoroutine.done()) {
+ qWarning() << "QCoro::LazyTask destroyed before it was awaited!";
+ }
+ #endif
+}
+
+template<typename T>
+inline auto LazyTask<T>::operator co_await() const noexcept {
+ //! Specialization of the TaskAwaiterBase that returns the promise result by value
+ class TaskAwaiter : public detail::TaskAwaiterBase<promise_type> {
+ public:
+ TaskAwaiter(std::coroutine_handle<promise_type> thisTask)
+ : detail::TaskAwaiterBase<promise_type>{thisTask} {}
+
+ auto await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ detail::TaskAwaiterBase<promise_type>::await_suspend(awaitingCoroutine);
+ // Return handle to the lazy task, so that it gets automatically resumed.
+ return this->mAwaitedCoroutine;
+ }
+
+ auto await_resume() {
+ Q_ASSERT(this->mAwaitedCoroutine);
+ if constexpr (!std::is_void_v<T>) {
+ return std::move(this->mAwaitedCoroutine.promise().result());
+ } else {
+ // Wil re-throw exception, if any is stored
+ this->mAwaitedCoroutine.promise().result();
+ }
+ }
+ };
+
+ return TaskAwaiter{this->mCoroutine};
+}
+
+} // namespace QCoro
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+namespace QCoro
+{
+
+} // namespace QCoro
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+#include <QDebug>
+
+namespace QCoro::detail
+{
+
+template<typename Promise>
+inline bool TaskAwaiterBase<Promise>::await_ready() const noexcept {
+ return mAwaitedCoroutine && mAwaitedCoroutine.done();
+}
+
+template<typename Promise>
+inline void TaskAwaiterBase<Promise>::await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ if (!mAwaitedCoroutine) {
+ qWarning() << "QCoro::Task: Awaiting a default-constructed or a moved-from QCoro::Task<> - this will hang forever!";
+ return;
+ }
+
+ mAwaitedCoroutine.promise().addAwaitingCoroutine(awaitingCoroutine);
+}
+
+template<typename Promise>
+inline TaskAwaiterBase<Promise>::TaskAwaiterBase(std::coroutine_handle<Promise> awaitedCoroutine)
+ : mAwaitedCoroutine(awaitedCoroutine) {}
+
+} // namespace QCoro::detail
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+#include <optional>
+#include <QDebug>
+
+namespace QCoro::detail {
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline TaskBase<T, TaskImpl, PromiseType>::TaskBase(std::coroutine_handle<PromiseType> coroutine) : mCoroutine(coroutine) {
+ mCoroutine.promise().refCoroutine();
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline TaskBase<T, TaskImpl, PromiseType>::TaskBase(TaskBase &&other) noexcept : mCoroutine(other.mCoroutine) {
+ other.mCoroutine = nullptr;
+}
+
+//! The task can be move-assigned.
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline auto TaskBase<T, TaskImpl, PromiseType>::operator=(TaskBase &&other) noexcept -> TaskBase & {
+ if (std::addressof(other) != this) {
+ if (mCoroutine) {
+ mCoroutine.promise().derefCoroutine();
+ }
+
+ mCoroutine = other.mCoroutine;
+ other.mCoroutine = nullptr;
+ }
+ return *this;
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline TaskBase<T, TaskImpl, PromiseType>::~TaskBase() {
+ if (mCoroutine) {
+ mCoroutine.promise().derefCoroutine();
+ }
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline bool TaskBase<T, TaskImpl, PromiseType>::isReady() const {
+ return !mCoroutine || mCoroutine.done();
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+inline auto TaskBase<T, TaskImpl, PromiseType>::operator co_await() const noexcept {
+ //! Specialization of the TaskAwaiterBase that returns the promise result by value
+ class TaskAwaiter : public detail::TaskAwaiterBase<PromiseType> {
+ public:
+ TaskAwaiter(std::coroutine_handle<PromiseType> awaitedCoroutine)
+ : detail::TaskAwaiterBase<PromiseType>{awaitedCoroutine} {}
+
+ //! Called when the co_awaited coroutine is resumed.
+ /*
+ * \return the result from the coroutine's promise, factically the
+ * value co_returned by the coroutine. */
+ auto await_resume() {
+ Q_ASSERT(this->mAwaitedCoroutine);
+ if constexpr (!std::is_void_v<T>) {
+ return std::move(this->mAwaitedCoroutine.promise().result());
+ } else {
+ // Wil re-throw exception, if any is stored
+ this->mAwaitedCoroutine.promise().result();
+ }
+ }
+ };
+
+ return TaskAwaiter{this->mCoroutine};
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename ThenCallback>
+requires (std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>))
+inline auto TaskBase<T, TaskImpl, PromiseType>::then(ThenCallback &&callback) & {
+ // Provide a custom error handler that simply re-throws the current exception
+ return thenImplRef(*this, std::forward<ThenCallback>(callback), [](const auto &) { throw; });
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename ThenCallback>
+requires (std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>))
+inline auto TaskBase<T, TaskImpl, PromiseType>::then(ThenCallback &&callback) && {
+ // When chaining LazyTask continuation to a temporary (xvalue) LazyTask, the temporary LazyTask must be captured in
+ // thenImpl() by value as (as an argument), since thenImpl() is suspended immediately (it's a lazy task) and the temporary
+ // would be destroyed before the thenImpl() is resumed and has a chance to consume/co_await the temporary LazyTask.
+ return thenImpl<TaskBase>(std::move(*this), std::forward<ThenCallback>(callback), [](const auto &) { throw; });
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename ThenCallback, typename ErrorCallback>
+requires ((std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>)) &&
+ std::is_invocable_v<ErrorCallback, const std::exception &>)
+inline auto TaskBase<T, TaskImpl, PromiseType>::then(ThenCallback &&callback, ErrorCallback &&errorCallback) & {
+ return thenImplRef(*this, std::forward<ThenCallback>(callback), std::forward<ErrorCallback>(errorCallback));
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename ThenCallback, typename ErrorCallback>
+requires ((std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>)) &&
+ std::is_invocable_v<ErrorCallback, const std::exception &>)
+inline auto TaskBase<T, TaskImpl, PromiseType>::then(ThenCallback &&callback, ErrorCallback &&errorCallback) && {
+ return thenImpl<TaskBase>(std::move(*this), std::forward<ThenCallback>(callback), std::forward<ErrorCallback>(errorCallback));
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename ThenCallback, typename ... Args>
+inline auto TaskBase<T, TaskImpl, PromiseType>::invokeCb(ThenCallback &&callback, [[maybe_unused]] Args && ... args) {
+ if constexpr (std::is_invocable_v<ThenCallback, Args ...>) {
+ return callback(std::forward<Args>(args) ...);
+ } else {
+ return callback();
+ }
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename R, typename ErrorCallback, typename U>
+inline auto TaskBase<T, TaskImpl, PromiseType>::handleException(ErrorCallback &errCb, const std::exception &exception) -> U {
+ errCb(exception);
+ if constexpr (!std::is_void_v<U>) {
+ return U{};
+ }
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename TaskT, typename ThenCallback, typename ErrorCallback, typename R>
+inline auto TaskBase<T, TaskImpl, PromiseType>::thenImpl(TaskT task_, ThenCallback &&thenCallback, ErrorCallback &&errorCallback) -> std::conditional_t<detail::isTask_v<R>, R, TaskImpl<R>> {
+ auto thenCb = std::forward<ThenCallback>(thenCallback);
+ auto errCb = std::forward<ErrorCallback>(errorCallback);
+ const auto &task = static_cast<const TaskImpl<T> &>(task_);
+
+ if constexpr (std::is_void_v<typename TaskImpl<T>::value_type>) {
+ try {
+ co_await task;
+ } catch (const std::exception &e) {
+ co_return handleException<R>(errCb, e);
+ }
+ if constexpr (detail::isTask_v<R>) {
+ co_return co_await invokeCb(thenCb);
+ } else {
+ co_return invokeCb(thenCb);
+ }
+ } else {
+ std::optional<T> value;
+ try {
+ value.emplace(std::move(co_await task));
+ } catch (const std::exception &e) {
+ co_return handleException<R>(errCb, e);
+ }
+ if constexpr (detail::isTask_v<R>) {
+ co_return co_await invokeCb(thenCb, std::move(*value));
+ } else {
+ co_return invokeCb(thenCb, std::move(*value));
+ }
+ }
+}
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+template<typename TaskT, typename ThenCallback, typename ErrorCallback, typename R>
+inline auto TaskBase<T, TaskImpl, PromiseType>::thenImplRef(TaskT &task_, ThenCallback &&thenCallback, ErrorCallback &&errorCallback) -> std::conditional_t<detail::isTask_v<R>, R, TaskImpl<R>> {
+ auto thenCb = std::forward<ThenCallback>(thenCallback);
+ auto errCb = std::forward<ErrorCallback>(errorCallback);
+ const auto &task = static_cast<const TaskImpl<T> &>(task_);
+
+ if constexpr (std::is_void_v<typename TaskImpl<T>::value_type>) {
+ try {
+ co_await task;
+ } catch (const std::exception &e) {
+ co_return handleException<R>(errCb, e);
+ }
+ if constexpr (detail::isTask_v<R>) {
+ co_return co_await invokeCb(thenCb);
+ } else {
+ co_return invokeCb(thenCb);
+ }
+ } else {
+ std::optional<T> value;
+ try {
+ value.emplace(std::move(co_await task));
+ } catch (const std::exception &e) {
+ co_return handleException<R>(errCb, e);
+ }
+ if constexpr (detail::isTask_v<R>) {
+ co_return co_await invokeCb(thenCb, std::move(*value));
+ } else {
+ co_return invokeCb(thenCb, std::move(*value));
+ }
+ }
+}
+
+
+
+} // namespace QCoro::detail
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+namespace QCoro::detail
+{
+
+inline TaskFinalSuspend::TaskFinalSuspend(const std::vector<std::coroutine_handle<>> &awaitingCoroutines)
+ : mAwaitingCoroutines(awaitingCoroutines) {}
+
+inline bool TaskFinalSuspend::await_ready() const noexcept {
+ return false;
+}
+
+template<typename Promise>
+inline void TaskFinalSuspend::await_suspend(std::coroutine_handle<Promise> finishedCoroutine) noexcept {
+ auto &promise = finishedCoroutine.promise();
+
+ for (auto &awaiter : mAwaitingCoroutines) {
+ awaiter.resume();
+ }
+ mAwaitingCoroutines.clear();
+
+ // The handle will be destroyed here only if the associated Task has already been destroyed
+ promise.derefCoroutine();
+}
+
+constexpr void TaskFinalSuspend::await_resume() const noexcept {}
+
+} // namespace QCoro::detail
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+#include <QtGlobal> // for Q_ASSERT
+
+namespace QCoro::detail
+{
+
+template<typename T>
+inline Task<T> TaskPromise<T>::get_return_object() noexcept {
+ return Task<T>{std::coroutine_handle<TaskPromise>::from_promise(*this)};
+}
+
+template<typename T>
+inline void TaskPromise<T>::unhandled_exception() {
+ mValue = std::current_exception();
+}
+
+template<typename T>
+inline void TaskPromise<T>::return_value(T &&value) noexcept {
+ mValue.template emplace<T>(std::forward<T>(value));
+}
+
+template<typename T>
+inline void TaskPromise<T>::return_value(const T &value) noexcept {
+ mValue = value;
+}
+
+template<typename T>
+template<typename U> requires QCoro::concepts::constructible_from<T, U>
+inline void TaskPromise<T>::return_value(U &&value) noexcept {
+ mValue = T(std::move(value));
+}
+
+template<typename T>
+inline T &TaskPromise<T>::result() & {
+ if (std::holds_alternative<std::exception_ptr>(mValue)) {
+ Q_ASSERT(std::get<std::exception_ptr>(mValue) != nullptr);
+ std::rethrow_exception(std::get<std::exception_ptr>(mValue));
+ }
+
+ return std::get<T>(mValue);
+}
+
+template<typename T>
+inline T &&TaskPromise<T>::result() && {
+ if (std::holds_alternative<std::exception_ptr>(mValue)) {
+ Q_ASSERT(std::get<std::exception_ptr>(mValue) != nullptr);
+ std::rethrow_exception(std::get<std::exception_ptr>(mValue));
+ }
+
+ return std::move(std::get<T>(mValue));
+}
+
+inline Task<void> TaskPromise<void>::get_return_object() noexcept {
+ return Task<void>{std::coroutine_handle<TaskPromise>::from_promise(*this)};
+}
+
+inline void TaskPromise<void>::unhandled_exception() {
+ mException = std::current_exception();
+}
+
+inline void TaskPromise<void>::return_void() noexcept {}
+
+inline void TaskPromise<void>::result() {
+ if (mException) {
+ std::rethrow_exception(mException);
+ }
+}
+
+} // namespace QCoro::detail
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+#include <coroutine>
+
+namespace QCoro::detail
+{
+
+inline TaskPromiseBase::TaskPromiseBase()
+ : mRefCount(1)
+{
+}
+
+inline std::suspend_never TaskPromiseBase::initial_suspend() const noexcept {
+ return {};
+}
+
+inline auto TaskPromiseBase::final_suspend() const noexcept {
+ return TaskFinalSuspend{mAwaitingCoroutines};
+}
+
+template<typename T, typename Awaiter>
+inline auto TaskPromiseBase::await_transform(T &&value) {
+ return Awaiter{std::forward<T>(value)};
+}
+
+template<Awaitable T>
+inline auto && TaskPromiseBase::await_transform(T &&awaitable) {
+ return std::forward<T>(awaitable);
+}
+
+template<Awaitable T>
+inline auto &TaskPromiseBase::await_transform(T &awaitable) {
+ return awaitable;
+}
+
+inline void TaskPromiseBase::addAwaitingCoroutine(std::coroutine_handle<> awaitingCoroutine) {
+ mAwaitingCoroutines.push_back(awaitingCoroutine);
+}
+
+inline bool TaskPromiseBase::hasAwaitingCoroutine() const {
+ return !mAwaitingCoroutines.empty();
+}
+
+inline void TaskPromiseBase::derefCoroutine() {
+ if (--mRefCount == 0) {
+ destroyCoroutine();
+ }
+}
+
+inline void TaskPromiseBase::refCoroutine() {
+ ++mRefCount;
+}
+
+inline void TaskPromiseBase::destroyCoroutine() {
+ mRefCount = 0;
+ auto handle = std::coroutine_handle<TaskPromiseBase>::from_promise(*this);
+ handle.destroy();
+}
+
+} // namespace QCoro::detail
--- /dev/null
+// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * Do NOT include this file directly - include the QCoroTask header instead!
+ */
+
+#pragma once
+
+#include "../qcorotask.h"
+
+#include <QEventLoop>
+
+#include <optional>
+
+namespace QCoro
+{
+
+namespace detail
+{
+
+template <typename Awaitable>
+requires TaskConvertible<Awaitable>
+auto toTask(Awaitable &&future) -> QCoro::Task<detail::convertible_awaitable_return_type_t<Awaitable>> {
+ co_return co_await future;
+}
+
+struct WaitContext {
+ QEventLoop loop;
+ bool coroutineFinished = false;
+ std::exception_ptr exception;
+};
+
+//! Helper class to run a coroutine in a nested event loop.
+/*!
+ * We cannot just use QTimer or QMetaObject::invokeMethod() to schedule the func lambda to be
+ * invoked from an event loop, because internally, Qt deallocates some structures when the
+ * lambda returns, which causes invalid memory access and potentially double-free corruption
+ * because the coroutine returns twice - once on suspend and once when it really finishes.
+ * So instead we do basically what Qt does internally, but we make sure to not delete th
+ * QFunctorSlotObjectWithNoArgs until after the event loop quits.
+ */
+template<Awaitable Awaitable>
+Task<> runCoroutine(WaitContext &context, Awaitable &&awaitable) {
+ try {
+ co_await awaitable;
+ } catch (...) {
+ context.exception = std::current_exception();
+ }
+ context.coroutineFinished = true;
+ context.loop.quit();
+}
+
+template<typename T, Awaitable Awaitable>
+Task<> runCoroutine(WaitContext &context, std::optional<T> &result, Awaitable &&awaitable) {
+ try {
+ result.emplace(co_await awaitable);
+ } catch (...) {
+ context.exception = std::current_exception();
+ }
+ context.coroutineFinished = true;
+ context.loop.quit();
+}
+
+template<typename T, Awaitable Awaitable>
+T waitFor(Awaitable &&awaitable) {
+ WaitContext context;
+ if constexpr (std::is_void_v<T>) {
+ runCoroutine(context, std::forward<Awaitable>(awaitable));
+ if (!context.coroutineFinished) {
+ context.loop.exec();
+ }
+ if (context.exception) {
+ std::rethrow_exception(context.exception);
+ }
+ } else {
+ std::optional<T> result;
+ runCoroutine(context, result, std::forward<Awaitable>(awaitable));
+ if (!context.coroutineFinished) {
+ context.loop.exec();
+ }
+ if (context.exception) {
+ std::rethrow_exception(context.exception);
+ }
+ return std::move(*result);
+ }
+}
+
+} // namespace detail
+
+template<typename T>
+inline T waitFor(QCoro::Task<T> &task) {
+ return detail::waitFor<T>(std::forward<QCoro::Task<T>>(task));
+}
+
+template<typename T>
+inline T waitFor(QCoro::Task<T> &&task) {
+ return detail::waitFor<T>(std::forward<QCoro::Task<T>>(task));
+}
+
+template<Awaitable Awaitable>
+inline auto waitFor(Awaitable &&awaitable) {
+ return detail::waitFor<detail::awaitable_return_type_t<Awaitable>>(std::forward<Awaitable>(awaitable));
+}
+
+} // namespace QCoro
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifndef QCORO_DEFAULT_MOVE
+#define QCORO_DEFAULT_MOVE(Class) \
+ Class(Class &&) noexcept = default; \
+ Class &operator=(Class &&) noexcept = default;
+#endif
--- /dev/null
+# SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME Network
+ SOURCES
+ qcoroabstractsocket.cpp
+ qcorolocalsocket.cpp
+ qcoronetworkreply.cpp
+ qcorotcpserver.cpp
+ CAMELCASE_HEADERS
+ QCoroNetwork
+ QCoroAbstractSocket
+ QCoroLocalSocket
+ QCoroNetworkReply
+ QCoroTcpServer
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro Core
+ QT_LINK_LIBRARIES
+ PUBLIC Core Network
+)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroabstractsocket.h"
+#include "qcoroiodevice_p.h"
+#include "qcorosignal.h"
+
+using namespace QCoro::detail;
+using namespace std::chrono_literals;
+
+namespace {
+
+class AbstractSocketReadySignalHelper : public WaitSignalHelper {
+ Q_OBJECT
+public:
+ explicit AbstractSocketReadySignalHelper(const QAbstractSocket *socket, void(QIODevice::*readySignal)())
+ : WaitSignalHelper(socket, readySignal)
+ , mStateChanged(connect(socket, &QAbstractSocket::stateChanged, this,
+ [this](QAbstractSocket::SocketState state) { handleStateChange(state, false); }))
+ {}
+
+ explicit AbstractSocketReadySignalHelper(const QAbstractSocket *socket, void(QIODevice::*readySignal)(qint64))
+ : WaitSignalHelper(socket, readySignal)
+ , mStateChanged(connect(socket, &QAbstractSocket::stateChanged, this,
+ [this](QAbstractSocket::SocketState state) {
+ handleStateChange(state, static_cast<qint64>(0)); }))
+ {}
+
+private:
+ template<typename T>
+ void handleStateChange(QAbstractSocket::SocketState state, T result) {
+ if (state == QAbstractSocket::ClosingState || state == QAbstractSocket::UnconnectedState) {
+ disconnect(mStateChanged);
+ emitReady(result);
+ }
+ }
+
+private:
+ QMetaObject::Connection mStateChanged;
+};
+
+} // namespace
+
+QCoroAbstractSocket::QCoroAbstractSocket(QAbstractSocket *socket)
+ : QCoroIODevice(socket) {}
+
+QCoro::Task<std::optional<bool>> QCoroAbstractSocket::waitForReadyReadImpl(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QAbstractSocket *>(mDevice.data());
+ if (socket->state() != QAbstractSocket::ConnectedState) {
+ co_return false;
+ }
+
+ AbstractSocketReadySignalHelper helper(socket, &QIODevice::readyRead);
+ co_return co_await qCoro(&helper, qOverload<bool>(&WaitSignalHelper::ready), timeout);
+}
+
+QCoro::Task<std::optional<qint64>> QCoroAbstractSocket::waitForBytesWrittenImpl(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QAbstractSocket *>(mDevice.data());
+ if (socket->state() != QAbstractSocket::ConnectedState) {
+ co_return std::nullopt;
+ }
+
+ AbstractSocketReadySignalHelper helper(socket, &QIODevice::bytesWritten);
+ co_return co_await qCoro(&helper, qOverload<qint64>(&WaitSignalHelper::ready), timeout);
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::waitForConnected(int timeout_msecs) {
+ return waitForConnected(std::chrono::milliseconds{timeout_msecs});
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::waitForConnected(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QAbstractSocket *>(mDevice.data());
+ if (socket->state() == QAbstractSocket::ConnectedState) {
+ co_return true;
+ }
+
+ const auto result = co_await qCoro(socket, &QAbstractSocket::connected, timeout);
+ co_return result.has_value();
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::waitForDisconnected(int timeout_msecs) {
+ return waitForDisconnected(std::chrono::milliseconds{timeout_msecs});
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::waitForDisconnected(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QAbstractSocket *>(mDevice.data());
+ if (socket->state() == QAbstractSocket::UnconnectedState) {
+ co_return false;
+ }
+
+ const auto result = co_await qCoro(socket, &QAbstractSocket::disconnected, timeout);
+ co_return result.has_value();
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::connectToHost(const QString &hostName, quint16 port,
+ QIODevice::OpenMode openMode, QAbstractSocket::NetworkLayerProtocol protocol,
+ std::chrono::milliseconds timeout) {
+ static_cast<QAbstractSocket *>(mDevice.data())->connectToHost(hostName, port, openMode, protocol);
+ return waitForConnected(timeout);
+}
+
+QCoro::Task<bool> QCoroAbstractSocket::connectToHost(const QHostAddress &address, quint16 port,
+ QIODevice::OpenMode openMode, std::chrono::milliseconds timeout) {
+ static_cast<QAbstractSocket *>(mDevice.data())->connectToHost(address, port, openMode);
+ return waitForConnected(timeout);
+}
+
+#include "qcoroabstractsocket.moc"
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "waitoperationbase_p.h"
+#include "qcoroiodevice.h"
+#include "qcoronetwork_export.h"
+
+#include <QPointer>
+#include <QAbstractSocket>
+
+#include <chrono>
+
+class QAbstractSocket;
+
+namespace QCoro::detail {
+
+using namespace std::chrono_literals;
+
+//! QAbstractSocket wrapper with co_awaitable-friendly API.
+class QCORONETWORK_EXPORT QCoroAbstractSocket final : public QCoroIODevice {
+public:
+ explicit QCoroAbstractSocket(QAbstractSocket *socket);
+
+ //! Co_awaitable equivalent to [`QAbstractSocket::waitForConnected()`][qtdoc-qabstractsocket-waitForConnected].
+ /*!
+ * Waits until the socket is connected, up to \c timeout_msecs milliseconds. If the connection has been
+ * established, the coroutine returns `true`, otherwise it retuns `false`.
+ *
+ * If the timeout is -1, the operation will never time out.
+ *
+ * [qtdoc-qabstractsocket-waitForConnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForConnected
+ */
+ Task<bool> waitForConnected(int timeout_msecs = 30'000);
+
+ //! Co_awaitable equivalent to [`QAbstractSocket::waitForConnected()`][qtdoc-qabstractsocket-waitForConnected].
+ /*!
+ * Waits until the socket is connected, up to \c timeout_msecs milliseconds. If the connection has been
+ * established, the coroutine returns `true`, otherwise it retuns `false`.
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the timeout is -1, the operation will never time out.
+ *
+ *
+ * [qtdoc-qabstractsocket-waitForConnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForConnected
+ */
+ Task<bool> waitForConnected(std::chrono::milliseconds timeout);
+
+ //! Co_awaitable equivalent to [`QAbstractSocket::waitForDisconnected()`][qtdoc-qabstractsocket-waitForDisconnected].
+ /*!
+ * Waits until the socket has disconnected, up to \c timeout_msecs milliseconds. If the connection was
+ * successfully disconnected, returns `true`, otherwise returns `false` (if the operation timed out,
+ * if an error occurred or if the `QAbstractSocket` is already disconnected).
+ *
+ * If the timeout is -1, the operation will never time out.
+ *
+ * [qtdoc-qabstractsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qabstractsocket.hmtl#waitForDisconnected
+ */
+ Task<bool> waitForDisconnected(int timeout_msecs = 30'000);
+
+ //! Co_awaitable equivalent to [`QAbstractSocket::waitForDisconnected()`][qtdoc-qabstractsocket-waitForDisconnected].
+ /*!
+ * Waits until the socket has disconnected, up to \c timeout_msecs milliseconds. If the connection was
+ * successfully disconnected, returns `true`, otherwise returns `false` (if the operation timed out,
+ * if an error occurred or if the `QAbstractSocket` is already disconnected).
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the timeout is -1, the operation will never time out.
+ *
+ * [qtdoc-qabstractsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qabstractsocket.hmtl#waitForDisconnected
+ */
+ Task<bool> waitForDisconnected(std::chrono::milliseconds timeout);
+
+ //! Connects to server and waits until the connection is established.
+ /*!
+ * Equivalent to calling [`QAbstractSocket::connecToServer`][qdoc-qabstractsocket-connecToHost]
+ * followed by [`QAbstractSocket::waitForConnected`][qdoc-qabstractsocket-waitForConnected].
+ *
+ * Returns `true` if the connection has been successfully established withint he given timeout,
+ * `false` otherwise. If the timeout is -1, the operation will never time out.
+ *
+ * [qtdoc-qabstractsocket-connectToHost]: https://doc.qt.io/qt-5/qabstractsocket.html#connectToHost
+ * [qtdoc-qabstractsocket-waitForConnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForConnected
+ */
+ Task<bool> connectToHost(const QString &hostName, quint16 port,
+ QIODevice::OpenMode openMode = QIODevice::ReadWrite,
+ QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::AnyIPProtocol,
+ std::chrono::milliseconds timeout = std::chrono::seconds{30});
+
+ //! Connects to server and waits until the connection is established.
+ /*!
+ * Equivalent to calling [`QAbstractSocket::connecToServer`][qdoc-qabstractsocket-connecToHost-1]
+ * followed by [`QAbstractSocket::waitForConnected`][qdoc-qabstractsocket-waitForConnected].
+ *
+ * Returns `true` if the connection has been successfully established within the given timeout,
+ * `false` otherwise. If the timeout is -1, the operation will never time out.
+ *
+ * [qtdoc-qabstractsocket-waitForConnected]: https://doc.qt.io/qt-5/qabstractsocket.html#waitForConnected
+ * [qtdoc-qabstractsocket-connectToHost-1]: https://doc.qt.io/qt-5/qabstractsocket.html#connectToHost-1
+ */
+ Task<bool> connectToHost(const QHostAddress &address, quint16 port,
+ QIODevice::OpenMode openMode = QIODevice::ReadWrite,
+ std::chrono::milliseconds timeout = std::chrono::seconds{30});
+
+private:
+ Task<std::optional<bool>> waitForReadyReadImpl(std::chrono::milliseconds timeout) override;
+ Task<std::optional<qint64>> waitForBytesWrittenImpl(std::chrono::milliseconds timeout) override;
+};
+
+} // namespace QCoro::detail
+
+//! Returns a coroutine-friendly wrapper for QAbstractSocket object.
+/*!
+ * Returns a wrapper for the QAbstractSocket \c s that provides coroutine-friendly
+ * way to co_await the socket to connect and disconnect.
+ *
+ * @see docs/reference/qabstractsocket.md
+ */
+inline auto qCoro(QAbstractSocket &s) noexcept {
+ return QCoro::detail::QCoroAbstractSocket{&s};
+}
+//! \copydoc qCoro(QAbstractSocket &s) noexcept
+inline auto qCoro(QAbstractSocket *s) noexcept {
+ return QCoro::detail::QCoroAbstractSocket{s};
+}
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorolocalsocket.h"
+#include "qcorosignal.h"
+#include "qcoroiodevice_p.h"
+
+#include <chrono>
+#include <functional>
+
+using namespace QCoro::detail;
+
+using namespace std::chrono_literals;
+
+namespace {
+
+class SocketConnectedHelper : public QObject {
+ Q_OBJECT
+public:
+ SocketConnectedHelper(const QLocalSocket *socket, void(QLocalSocket::*signal)())
+ : QObject()
+ , mSignal(connect(socket, signal, this, [this]() { emitReady(true); }))
+ , mStateChange(connect(socket, &QLocalSocket::stateChanged, this, [this](auto state) {
+ if (state == QLocalSocket::UnconnectedState) {
+ emitReady(false);
+ }
+ }))
+ {}
+
+Q_SIGNALS:
+ void ready(bool result);
+
+private:
+ void emitReady(bool result) {
+ disconnect(mSignal);
+ disconnect(mStateChange);
+ Q_EMIT ready(result);
+ }
+private:
+ QMetaObject::Connection mSignal;
+ QMetaObject::Connection mStateChange;
+};
+
+class LocalSocketReadySignalHelper : public WaitSignalHelper {
+ Q_OBJECT
+public:
+ LocalSocketReadySignalHelper(const QLocalSocket *socket, void(QIODevice::*signal)())
+ : WaitSignalHelper(socket, signal)
+ , mStateChange(connect(socket, &QLocalSocket::stateChanged, this,
+ [this](auto state) { stateChanged(state, false); }))
+ {}
+ LocalSocketReadySignalHelper(const QLocalSocket *socket, void(QIODevice::*signal)(qint64))
+ : WaitSignalHelper(socket, signal)
+ , mStateChange(connect(socket, &QLocalSocket::stateChanged, this,
+ [this](auto state) { stateChanged(state, static_cast<qint64>(0)); }))
+ {}
+
+private:
+ template<typename T>
+ void stateChanged(QLocalSocket::LocalSocketState state, T result) {
+ if (state != QLocalSocket::ConnectedState) {
+ disconnect(mStateChange);
+ emitReady(result);
+ }
+ }
+
+private:
+ QMetaObject::Connection mStateChange;
+};
+
+} // namespace
+
+QCoroLocalSocket::QCoroLocalSocket(QLocalSocket *socket)
+ : QCoroIODevice(socket)
+{}
+
+QCoro::Task<std::optional<bool>> QCoroLocalSocket::waitForReadyReadImpl(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QLocalSocket *>(mDevice.data());
+ if (socket->state() != QLocalSocket::ConnectedState) {
+ co_return false;
+ }
+ LocalSocketReadySignalHelper helper(socket, &QLocalSocket::readyRead);
+ co_return co_await qCoro(&helper, qOverload<bool>(&LocalSocketReadySignalHelper::ready), timeout);
+}
+
+QCoro::Task<std::optional<qint64>> QCoroLocalSocket::waitForBytesWrittenImpl(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QLocalSocket *>(mDevice.data());
+ if (socket->state() != QLocalSocket::ConnectedState) {
+ co_return std::nullopt;
+ }
+ LocalSocketReadySignalHelper helper(socket, &QLocalSocket::bytesWritten);
+ co_return co_await qCoro(&helper, qOverload<qint64>(&LocalSocketReadySignalHelper::ready), timeout);
+}
+
+QCoro::Task<bool> QCoroLocalSocket::waitForConnected(int timeout_msecs) {
+ return waitForConnected(std::chrono::milliseconds(timeout_msecs));
+}
+
+QCoro::Task<bool> QCoroLocalSocket::waitForConnected(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QLocalSocket *>(mDevice.data());
+ if (socket->state() == QLocalSocket::ConnectedState) {
+ co_return true;
+ }
+ SocketConnectedHelper helper(socket, &QLocalSocket::connected);
+ const auto result = co_await qCoro(&helper, &SocketConnectedHelper::ready, timeout);
+ co_return result.value_or(false);
+}
+
+QCoro::Task<bool> QCoroLocalSocket::waitForDisconnected(int timeout_msecs) {
+ return waitForDisconnected(std::chrono::milliseconds(timeout_msecs));
+}
+
+QCoro::Task<bool> QCoroLocalSocket::waitForDisconnected(std::chrono::milliseconds timeout) {
+ const auto *socket = static_cast<QLocalSocket *>(mDevice.data());
+ if (socket->state() == QLocalSocket::UnconnectedState) {
+ co_return false;
+ }
+ const auto result = co_await qCoro(socket, &QLocalSocket::disconnected, timeout);
+ co_return result.has_value();
+}
+
+QCoro::Task<bool> QCoroLocalSocket::connectToServer(QIODevice::OpenMode openMode,
+ std::chrono::milliseconds timeout) {
+ static_cast<QLocalSocket *>(mDevice.data())->connectToServer(openMode);
+ return waitForConnected(timeout);
+}
+
+QCoro::Task<bool> QCoroLocalSocket::connectToServer(const QString &name, QIODevice::OpenMode openMode,
+ std::chrono::milliseconds timeout) {
+ static_cast<QLocalSocket *>(mDevice.data())->connectToServer(name, openMode);
+ return waitForConnected(timeout);
+}
+
+#include "qcorolocalsocket.moc"
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "waitoperationbase_p.h"
+#include "qcoroiodevice.h"
+#include "qcoronetwork_export.h"
+
+#include <QLocalSocket>
+
+#include <chrono>
+
+namespace QCoro::detail {
+
+using namespace std::chrono_literals;
+
+//! QLocalSocket wrapper with co_awaitable-friendly API.
+class QCORONETWORK_EXPORT QCoroLocalSocket : public QCoroIODevice {
+public:
+ explicit QCoroLocalSocket(QLocalSocket *socket);
+
+ /**
+ * \brief Co_awaitable equivalent to [`QLocalSocket::waitForConnected()`][qtdoc-qlocalsocket-waitForConnected].
+ *
+ * Waits for at most \c timeout_msecs milliseconds. If the timeout is -1, the call will never timeout.
+ *
+ * \return Returns `true` when successfully connected, `false` is an error occured or the connection
+ * wasn't established within the given timeout.
+ *
+ * [qtdoc-qlocalsocket-waitForConnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForConnected
+ */
+ Task<bool> waitForConnected(int timeout_msecs = 30'000);
+
+ /**
+ * \brief Co_awaitable equivalent to [`QLocalSocket::waitForConnected()`][qtdoc-qlocalsocket-waitForConnected].
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1, the call will never time out.
+ *
+ * \return Returns `true` when successfully connected, `false` is an error occured or the connection
+ * wasn't established within the given timeout.
+ *
+ * [qtdoc-qlocalsocket-waitForConnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForConnected
+ */
+ Task<bool> waitForConnected(std::chrono::milliseconds timeout);
+
+ /**
+ * \brief Co_awaitable equivalent to [`QLocalSocket::waitForDisconnected()`][qtdoc-qlocalsocket-waitForDisconnected].
+ *
+ * Waits for at most \c timeout_msecds milliseconds. If the timeout is -1, the call will never time out.
+ *
+ * \returns Returns `true` when the socket has been disconnected successfully. If the socket
+ * wasn't connected, or doesn't disconnected within the specified timeout, the coroutine returns
+ * `false`.
+ *
+ * [qtdoc-qlocalsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qlocalsocket.hmtl#waitForDisconnected
+ */
+ Task<bool> waitForDisconnected(int timeout_msecs = 30'000);
+
+ /**
+ * \brief Co_awaitable equivalent to [`QLocalSocket::waitForDisconnected()`][qtdoc-qlocalsocket-waitForDisconnected].
+ *
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1, the call will never time out.
+ *
+ * \returns Returns `true` when the socket has been disconnected successfully. If the socket
+ * wasn't connected, or doesn't disconnected within the specified timeout, the coroutine returns
+ * `false`.
+ *
+ * [qtdoc-qlocalsocket-waitForDisconnected]: https://doc.qt.io/qt-5/qlocalsocket.hmtl#waitForDisconnected
+ */
+ Task<bool> waitForDisconnected(std::chrono::milliseconds timeout);
+
+ /**
+ * \brief Connects to server and waits until the connection is established.
+ *
+ * Equivalent to calling [`QLocalSocket::connecToServer`][qdoc-qlocalsocket-connecToServer]
+ * followed by [`QLocalSocket::waitForConnected`][qdoc-qlocalsocket-waitForConnected].
+ *
+ * Waits for at most \c timeout milliseconds. If the \c timeout is -1, the call will never time out.
+ *
+ * \return Returns `true` when connection is successfully established, `false` otherwise.
+ *
+ * [qtdoc-qlocalsocket-connectToServer]: https://doc.qt.io/qt-5/qlocalsocket.html#connectToServer
+ * [qtdoc-qlocalsocket-waitForConnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForConnected
+ */
+ Task<bool> connectToServer(QIODevice::OpenMode openMode = QIODevice::ReadWrite,
+ std::chrono::milliseconds timeout = std::chrono::seconds{30});
+
+ /**
+ * \brief Connects to server and waits until the connection is established.
+ *
+ * Equivalent to calling [`QLocalSocket::connecToServer`][qdoc-qlocalsocket-connecToServer]
+ * followed by [`QLocalSocket::waitForConnected`][qdoc-qlocalsocket-waitForConnected].
+ *
+ * Waits for at most \c timeout milliseconds. If the \c timeout is -1, the call will never time out.
+ *
+ * \returns Returns `true` when connection is successfully established, `false` otherwise.
+ *
+ * [qtdoc-qlocalsocket-connectToServer-1]: https://doc.qt.io/qt-5/qlocalsocket.html#connectToServer-1
+ * [qtdoc-qlocalsocket-waitForConnected]: https://doc.qt.io/qt-5/qlocalsocket.html#waitForConnected
+ */
+ Task<bool> connectToServer(const QString &name, QIODevice::OpenMode openMode = QIODevice::ReadWrite,
+ std::chrono::milliseconds timeout = std::chrono::seconds{30});
+
+private:
+ Task<std::optional<bool>> waitForReadyReadImpl(std::chrono::milliseconds timeout) override;
+ Task<std::optional<qint64>> waitForBytesWrittenImpl(std::chrono::milliseconds timeout) override;
+};
+
+} // namespace QCoro::detail
+
+//! Returns a coroutine-friendly wrapper for QLocalSocket object.
+/*!
+ * Returns a wrapper for the QLocalSocket \c s that provides coroutine-friendly
+ * way to co_await the socket to connect and disconnect.
+ *
+ * @see docs/reference/qlocalsocket.md
+ */
+inline auto qCoro(QLocalSocket &s) noexcept {
+ return QCoro::detail::QCoroLocalSocket{&s};
+}
+//! \copydoc qCoro(QLocalSocket &s) noexcept
+inline auto qCoro(QLocalSocket *s) noexcept {
+ return QCoro::detail::QCoroLocalSocket{s};
+}
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroabstractsocket.h"
+#include "qcorolocalsocket.h"
+#include "qcoronetworkreply.h"
+#include "qcorotcpserver.h"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoronetworkreply.h"
+#include "qcoroiodevice_p.h"
+#include "qcorosignal.h"
+
+using namespace QCoro::detail;
+
+namespace {
+
+class ReplyWaitSignalHelper : public WaitSignalHelper {
+ Q_OBJECT
+public:
+ ReplyWaitSignalHelper(const QNetworkReply *reply, void(QIODevice::*signal)())
+ : WaitSignalHelper(reply, signal)
+ , mError(connect(reply, &QNetworkReply::errorOccurred, this, [this]() { emitReady(false); }, Qt::QueuedConnection))
+ , mFinished(connect(reply, &QNetworkReply::finished, this, [this]() { emitReady(true); }, Qt::QueuedConnection))
+ {}
+ ReplyWaitSignalHelper(const QNetworkReply *reply, void(QIODevice::*signal)(qint64))
+ : WaitSignalHelper(reply, signal)
+ , mError(connect(reply, &QNetworkReply::errorOccurred, this, [this]() { emitReady(0LL); }, Qt::QueuedConnection))
+ , mFinished(connect(reply, &QNetworkReply::finished, this, [this]() { emitReady(0LL); }, Qt::QueuedConnection))
+ {}
+
+private:
+ void cleanup() override {
+ disconnect(mError);
+ disconnect(mFinished);
+ WaitSignalHelper::cleanup();
+ }
+
+ QMetaObject::Connection mError;
+ QMetaObject::Connection mFinished;
+};
+
+} // namespace
+
+struct QCoroNetworkReply::WaitForFinishedOperation::Private {
+ static_assert(sizeof(std::unique_ptr<Private>) + sizeof(void*) == sizeof(QPointer<QNetworkReply>),
+ "QCoroNetworkReply::WaitForFinishedOperation is not BC with previous version, see comment in header");
+
+ Private(QPointer<QNetworkReply> reply)
+ : reply(reply)
+ {
+ // Ensure the signal is emitted in the same thread as the reply, not on the thread
+ // where the coroutine is resumed...
+ if (reply) {
+ dummy.moveToThread(reply->thread());
+ }
+ }
+
+ QPointer<QNetworkReply> reply;
+ QObject dummy;
+};
+
+QCoroNetworkReply::WaitForFinishedOperation::WaitForFinishedOperation(QPointer<QNetworkReply> reply)
+ : d(std::make_unique<Private>(reply))
+{
+}
+
+QCoroNetworkReply::WaitForFinishedOperation::~WaitForFinishedOperation() = default;
+
+bool QCoroNetworkReply::WaitForFinishedOperation::await_ready() const noexcept {
+ return !d->reply || d->reply->isFinished();
+}
+
+void QCoroNetworkReply::WaitForFinishedOperation::await_suspend(std::coroutine_handle<> awaitingCoroutine) {
+ if (d->reply) {
+ QObject::connect(d->reply, &QNetworkReply::finished, &d->dummy,
+ [awaitingCoroutine]() mutable { awaitingCoroutine.resume(); },
+ Qt::QueuedConnection);
+ } else {
+ awaitingCoroutine.resume();
+ }
+}
+
+QNetworkReply *QCoroNetworkReply::WaitForFinishedOperation::await_resume() const noexcept {
+ return d->reply;
+}
+
+QCoro::Task<std::optional<bool>> QCoroNetworkReply::waitForReadyReadImpl(std::chrono::milliseconds timeout) {
+ const auto *reply = static_cast<QNetworkReply *>(mDevice.data());
+ if (reply->isFinished()) {
+ co_return true;
+ }
+ ReplyWaitSignalHelper helper(reply, &QNetworkReply::readyRead);
+ co_return co_await qCoro(&helper, qOverload<bool>(&ReplyWaitSignalHelper::ready), timeout);
+}
+
+QCoro::Task<std::optional<qint64>> QCoroNetworkReply::waitForBytesWrittenImpl(std::chrono::milliseconds timeout) {
+ const auto *reply = static_cast<QNetworkReply *>(mDevice.data());
+ if (reply->isFinished()) {
+ co_return false;
+ }
+ ReplyWaitSignalHelper helper(reply, &QNetworkReply::bytesWritten);
+ co_return co_await qCoro(&helper, qOverload<qint64>(&ReplyWaitSignalHelper::ready), timeout);
+}
+
+QCoro::Task<bool> QCoroNetworkReply::waitForFinished(std::chrono::milliseconds timeout) {
+ const auto *reply = static_cast<QNetworkReply *>(mDevice.data());
+ if (reply->isFinished()) {
+ co_return true;
+ }
+
+ const auto result = co_await qCoro(reply, &QNetworkReply::finished, timeout);
+ co_return result.has_value();
+}
+
+#include "qcoronetworkreply.moc"
\ No newline at end of file
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcoroiodevice.h"
+#include "qcoronetwork_export.h"
+
+#include <QNetworkReply>
+
+namespace QCoro::detail {
+
+class QCORONETWORK_EXPORT QCoroNetworkReply final : public QCoroIODevice {
+private:
+ class WaitForFinishedOperation final {
+ public:
+ explicit WaitForFinishedOperation(QPointer<QNetworkReply> reply);
+ ~WaitForFinishedOperation();
+
+ bool await_ready() const noexcept;
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine);
+ QNetworkReply *await_resume() const noexcept;
+
+ private:
+ struct Private;
+ std::unique_ptr<Private> d;
+ // BC: This class used to have a QPointer<QNetworkReply>, which is 2*sizeof(void*),
+ // while std::unique_ptr is only sizeof(void*), so this dummy one is to ensure binary
+ // compatibility of this class.
+ // FIXME: Remove in 1.0
+ // Silly gcc 11, cannot detect the dummy is unused and warns about unused attribute
+ #if __GNUC__ > 11 || defined(__clang__) || defined(_MSC_VER)
+ [[maybe_unused]]
+ #endif
+ void *dummy = nullptr;
+ };
+
+ friend struct awaiter_type<QNetworkReply *>;
+public:
+ using QCoroIODevice::QCoroIODevice;
+
+ /**
+ * \brief Waits for the reply to finish.
+ *
+ * Waits for at most \c timeout milliseconds. If the \c timeout is -1, the call will never
+ * time out.
+ *
+ * \return Returns `true` if the reply has finished (with or without an error), `false` if
+ * the wait has timed out.
+ */
+ Task<bool> waitForFinished(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+private:
+ Task<std::optional<bool>> waitForReadyReadImpl(std::chrono::milliseconds timeout) override;
+ Task<std::optional<qint64>> waitForBytesWrittenImpl(std::chrono::milliseconds timeout) override;
+};
+
+} // namespace QCoro::detail
+
+//! Returns a coroutine-friendly wrapper for QNetworkReply object.
+/*!
+ * Returns a wrapper for the QNetworkReply \c s that provides coroutine-friendly
+ * way to co_await read and write operations.
+ *
+ * @see docs/reference/qnetworkreply.md
+ */
+inline auto qCoro(QNetworkReply &s) noexcept {
+ return QCoro::detail::QCoroNetworkReply{&s};
+}
+//! \copydoc qCoro(QAbstractSocket &s) noexcept
+inline auto qCoro(QNetworkReply *s) noexcept {
+ return QCoro::detail::QCoroNetworkReply{s};
+}
+
+/*! \cond internal */
+namespace QCoro::detail {
+
+template<>
+struct awaiter_type<QNetworkReply *> {
+ using type = QCoroNetworkReply::WaitForFinishedOperation;
+};
+
+} // namespace QCoro::detail
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorotcpserver.h"
+#include "qcorosignal.h"
+
+#include <QTcpServer>
+
+using namespace QCoro::detail;
+
+QCoroTcpServer::WaitForNewConnectionOperation::WaitForNewConnectionOperation(QTcpServer *server, int timeout_msecs)
+ : WaitOperationBase(server, timeout_msecs) {}
+
+bool QCoroTcpServer::WaitForNewConnectionOperation::await_ready() const noexcept {
+ return !mObj || mObj->hasPendingConnections();
+}
+
+void QCoroTcpServer::WaitForNewConnectionOperation::await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept {
+ mConn = QObject::connect(mObj, &QTcpServer::newConnection,
+ std::bind(&WaitForNewConnectionOperation::resume, this, awaitingCoroutine));
+ startTimeoutTimer(awaitingCoroutine);
+}
+
+QTcpSocket *QCoroTcpServer::WaitForNewConnectionOperation::await_resume() {
+ return mTimedOut ? nullptr : mObj->nextPendingConnection();
+}
+
+QCoroTcpServer::QCoroTcpServer(QTcpServer *server)
+ : mServer(server)
+{}
+
+QCoro::Task<QTcpSocket *> QCoroTcpServer::waitForNewConnection(int timeout_msecs) {
+ return waitForNewConnection(std::chrono::milliseconds(timeout_msecs));
+}
+
+QCoro::Task<QTcpSocket *> QCoroTcpServer::waitForNewConnection(std::chrono::milliseconds timeout) {
+ const auto server = mServer;
+ if (!server->isListening()) {
+ co_return nullptr;
+ }
+ if (server->hasPendingConnections()) {
+ co_return server->nextPendingConnection();
+ }
+
+ const auto result = co_await qCoro(server.data(), &QTcpServer::newConnection, timeout);
+ if (result.has_value()) {
+ co_return server->nextPendingConnection();
+ }
+ co_return nullptr;
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "waitoperationbase_p.h"
+#include "qcoronetwork_export.h"
+
+#include <QPointer>
+
+#include <chrono>
+
+class QTcpServer;
+class QTcpSocket;
+
+namespace QCoro::detail {
+
+using namespace std::chrono_literals;
+
+//! QTcpServer wrapper with co_awaitable-friendly API.
+class QCORONETWORK_EXPORT QCoroTcpServer {
+ //! An Awaitable that suspends the coroutine until new connection is available
+ class WaitForNewConnectionOperation final : public WaitOperationBase<QTcpServer> {
+ public:
+ WaitForNewConnectionOperation(QTcpServer *server, int timeout_msecs = 30'000);
+ bool await_ready() const noexcept;
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept;
+ QTcpSocket *await_resume();
+ };
+
+public:
+ ///! Constructor.
+ explicit QCoroTcpServer(QTcpServer *server);
+
+ /**
+ * \brief co_awaitable equivalent to [`QTcpServer::waitForNewConnection()`][qtdoc-qtcpserver-waitForNewConnection].
+ *
+ * Waits for at most \c timeout_msecs milliseconds. If the timeout is -1, the call will never time out.
+ *
+ * \return Returns \c QTcpSocket of the pending connection. Returns `nullptr` if the server is not in
+ * the listening state or if the call times out.
+ *
+ * [qtdoc-qtcpserver-waitForNewConnection]: https://doc.qt.io/qt-5/qtcpserver.html#waitForNewConnection
+ */
+ Task<QTcpSocket *> waitForNewConnection(int timeout_msecs = 30'000);
+
+ //! Co_awaitable equivalent to [`QTcpServer::waitForNewConnection()`][qtdoc-qtcpserver-waitForNewConnection].
+ /*!
+ * Unlike the Qt version, this overload uses `std::chrono::milliseconds` to express the
+ * timeout rather than plain `int`. If the \c timeout is -1, the call will never time out.
+ *
+ * \return Returns \c QTcpSocket of the pending connection. Returns `nullptr` if the server is not in
+ * the listening state or if the call times out.
+ *
+ * [qtdoc-qtcpserver-waitForNewConnection]: https://doc.qt.io/qt-5/qtcpserver.html#waitForNewConnection
+ */
+ Task<QTcpSocket *> waitForNewConnection(std::chrono::milliseconds timeout);
+
+private:
+ QPointer<QTcpServer> mServer;
+};
+
+} // namespace QCoro::detail
+
+//! Returns a coroutine-friendly wrapper for QTcpServer object.
+/*!
+ * Returns a wrapper for QTcpServer \c s that provides coroutine-friendly way
+ * of co_awaiting new connections.
+ *
+ * @see docs/reference/qtcpserver.md
+ */
+inline auto qCoro(QTcpServer &s) noexcept {
+ return QCoro::detail::QCoroTcpServer{&s};
+}
+//! \copydoc qCoro(QTcpServer &s) noexcept
+inline auto qCoro(QTcpServer *s) noexcept {
+ return QCoro::detail::QCoroTcpServer{s};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "task.h"
+#include "qcorocore.h"
+#include "qcorodbus.h"
+#include "qcoronetwork.h"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+//
+// Based on cppcoro::async_generator (c) Lewis Baker, published under MIT license
+// https://github.com/lewissbaker/cppcoro/blob/master/include/cppcoro/async_generator.hpp
+
+#pragma once
+
+#include "coroutine.h"
+
+#include <iterator>
+#include <exception>
+
+namespace QCoro {
+
+template<typename T>
+class AsyncGenerator;
+
+namespace detail
+{
+
+template<typename T>
+class AsyncGeneratorIterator;
+class AsyncGeneratorYieldOperation;
+class AsyncGeneratorAdvanceOperation;
+
+class AsyncGeneratorPromiseBase {
+public:
+ AsyncGeneratorPromiseBase() noexcept = default;
+ AsyncGeneratorPromiseBase(const AsyncGeneratorPromiseBase &) = delete;
+ AsyncGeneratorPromiseBase(AsyncGeneratorPromiseBase &&) noexcept = default;
+ AsyncGeneratorPromiseBase& operator=(const AsyncGeneratorPromiseBase &) = delete;
+ AsyncGeneratorPromiseBase& operator=(AsyncGeneratorPromiseBase &&) noexcept = default;
+ ~AsyncGeneratorPromiseBase() = default;
+
+ std::suspend_always initial_suspend() const noexcept { return {}; }
+
+ AsyncGeneratorYieldOperation final_suspend() noexcept;
+
+ void unhandled_exception() noexcept {
+ m_exception = std::current_exception();
+ }
+
+ void return_void() noexcept {}
+
+ /// Query if the generator has reached the end of the sequence.
+ ///
+ /// Only valid to call after resuming from an awaited advance operation.
+ /// i.e. Either a begin() or iterator::operator++() operation.
+ bool finished() const noexcept {
+ return m_currentValue == nullptr;
+ }
+
+ void rethrow_if_unhandled_exception() {
+ if (m_exception) {
+ std::rethrow_exception(std::move(m_exception));
+ }
+ }
+
+protected:
+ AsyncGeneratorYieldOperation internal_yield_value() noexcept;
+
+private:
+ friend class AsyncGeneratorYieldOperation;
+ friend class AsyncGeneratorAdvanceOperation;
+ friend class IteratorAwaitableBase;
+
+ std::exception_ptr m_exception = nullptr;
+ std::coroutine_handle<> m_consumerCoroutine;
+
+protected:
+ void *m_currentValue = nullptr;
+};
+
+class AsyncGeneratorYieldOperation final {
+public:
+ explicit AsyncGeneratorYieldOperation(std::coroutine_handle<> consumer) noexcept
+ : m_consumer(consumer)
+ {}
+
+ bool await_ready() const noexcept { return false; }
+
+ std::coroutine_handle<> await_suspend([[maybe_unused]] std::coroutine_handle<> producer) noexcept {
+ return m_consumer;
+ }
+
+ void await_resume() noexcept {}
+
+private:
+ std::coroutine_handle<> m_consumer;
+};
+
+inline AsyncGeneratorYieldOperation AsyncGeneratorPromiseBase::final_suspend() noexcept {
+ m_currentValue = nullptr;
+ return internal_yield_value();
+}
+
+inline AsyncGeneratorYieldOperation AsyncGeneratorPromiseBase::internal_yield_value() noexcept {
+ return AsyncGeneratorYieldOperation{m_consumerCoroutine};
+}
+
+class IteratorAwaitableBase {
+protected:
+ explicit IteratorAwaitableBase(std::nullptr_t) noexcept {}
+ IteratorAwaitableBase(
+ AsyncGeneratorPromiseBase &promise,
+ std::coroutine_handle<> producerCoroutine) noexcept
+ : m_promise(std::addressof(promise))
+ , m_producerCoroutine(producerCoroutine) {}
+
+public:
+ bool await_ready() const noexcept {
+ return false;
+ }
+
+ std::coroutine_handle<> await_suspend(std::coroutine_handle<> consumerCoroutine) noexcept {
+ m_promise->m_consumerCoroutine = consumerCoroutine;
+ return m_producerCoroutine;
+ }
+
+protected:
+ AsyncGeneratorPromiseBase *m_promise = nullptr;
+ std::coroutine_handle<> m_producerCoroutine = {nullptr};
+};
+
+template<typename T>
+class AsyncGeneratorPromise final : public AsyncGeneratorPromiseBase {
+ using value_type = std::remove_reference_t<T>;
+public:
+ AsyncGeneratorPromise() noexcept = default;
+
+ AsyncGenerator<T> get_return_object() noexcept;
+
+ AsyncGeneratorYieldOperation yield_value(value_type &value) noexcept {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
+ m_currentValue = const_cast<std::remove_const_t<value_type> *>(std::addressof(value));
+ return internal_yield_value();
+ }
+
+ AsyncGeneratorYieldOperation yield_value(value_type &&value) noexcept {
+ return yield_value(value);
+ }
+
+ T &value() const noexcept {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
+ return *const_cast<value_type *>(static_cast<const value_type *>(m_currentValue));
+ }
+};
+
+/**
+ * @brief Promise type for asynchronous generator coroutine.
+ */
+template<typename T>
+class AsyncGeneratorPromise<T &&> final : public AsyncGeneratorPromiseBase {
+public:
+ AsyncGeneratorPromise() noexcept = default;
+ AsyncGenerator<T> get_return_object() noexcept;
+
+ AsyncGeneratorYieldOperation yield_value(T &&value) noexcept {
+ m_currentValue = std::addressof(value);
+ return internal_yield_value();
+ }
+
+ T &&value() const noexcept {
+ return std::move(*static_cast<T *>(m_currentValue));
+ }
+};
+
+} // namespace detail
+
+template<typename T>
+class AsyncGeneratorIterator final
+{
+ using promise_type = detail::AsyncGeneratorPromise<T>;
+ using handle_type = std::coroutine_handle<promise_type>;
+
+ class IncrementIteratorAwaitable final : public detail::IteratorAwaitableBase {
+ public:
+ explicit IncrementIteratorAwaitable(AsyncGeneratorIterator &iterator) noexcept
+ : detail::IteratorAwaitableBase(iterator.m_coroutine.promise(), iterator.m_coroutine),
+ m_iterator(iterator) {}
+
+ AsyncGeneratorIterator<T> &await_resume() {
+ if (m_promise->finished()) {
+ m_iterator = AsyncGeneratorIterator<T>{nullptr};
+ m_promise->rethrow_if_unhandled_exception();
+ }
+ return m_iterator;
+ }
+
+ private:
+ AsyncGeneratorIterator<T> &m_iterator;
+ };
+
+public:
+ using iterator_category = std::input_iterator_tag;
+ // Not sure what type should be used for difference_type as we don't
+ // allow calculating difference between two iterators.
+ using difference_type = std::ptrdiff_t;
+ using value_type = std::remove_reference_t<T>;
+ using reference = std::add_lvalue_reference_t<T>;
+ using pointer = std::add_pointer_t<value_type>;
+
+ explicit AsyncGeneratorIterator(std::nullptr_t) noexcept {}
+ explicit AsyncGeneratorIterator(handle_type coroutine) noexcept
+ : m_coroutine(coroutine)
+ {}
+
+ /**
+ * @brief Asynchronously increments the iterator
+ *
+ * Resume the generator coroutine and asynchonously waits for it
+ * to yield a value. Resolves to a new iterator with the new value.
+ *
+ * If the generator coroutine throws an exception, co_awaiting on the
+ * returned awaitable will re-throw the exception and the returned iterator
+ * will be invalid (past-the-end iterator).
+ **/
+ auto operator++() noexcept {
+ return IncrementIteratorAwaitable{*this};
+ }
+
+ /**
+ * Returns reference to the value yielded by the generator coroutine.
+ */
+ reference operator *() const noexcept {
+ return m_coroutine.promise().value();
+ }
+
+ bool operator==(const AsyncGeneratorIterator &other) const noexcept {
+ return m_coroutine == other.m_coroutine;
+ }
+
+ bool operator!=(const AsyncGeneratorIterator &other) const noexcept {
+ return !(*this == other);
+ }
+
+private:
+ handle_type m_coroutine = {nullptr};
+};
+
+/**
+ * @brief AsyncGenerator<T> is a return type for a generator coroutine.
+ *
+ * Generator is a function that yields a value and suspends its execution
+ * until another value is requested by the caller. The AsyncGenerator<T>
+ * allows the generator function to also contain `co_await`.
+ *
+ * The generator behaves like an iterable, so one can easilly iterate over
+ * values produced by the generator, until the generator finishes. If the
+ * AsyncGenerator<T> object is destroyed while the generator coroutine hasn't
+ * finished yet, the coroutine will be suspended and terminated.
+ *
+ * The generator function is resumed whenever the iterator is incremented.
+ * Therefore, the increment operation (operator++()) must be co_awaited.
+ * Retrieveing the generated value from the current iterator is a synchronous
+ * operation.
+ *
+ * @code
+ * // Asynchronously generates "count" random numbers
+ * QCoro::AsyncGenerator<int> randomValueGenerator(int count) {
+ * for (int i = 0; i < count; ++i) {
+ * co_yield co_await generateRandomNumber();
+ * }
+ * }
+ *
+ * QCoro::Task<int> randomSum(int count) {
+ * int sum = 0;
+ * QCORO_FOREACH(int rand, randomValueGenerator(10)) {
+ * sum += rand;
+ * }
+ * return sum;
+ * }
+ * @endcode
+ **/
+template<typename T>
+class [[nodiscard]] AsyncGenerator {
+public:
+ using promise_type = detail::AsyncGeneratorPromise<T>;
+ using iterator = AsyncGeneratorIterator<T>;
+
+ /// Constructs an empty asynchronous generator
+ AsyncGenerator() noexcept = default;
+
+ /// Constructs AsyncGenerator for given promise.
+ explicit AsyncGenerator(promise_type &promise) noexcept
+ : m_coroutine(std::coroutine_handle<promise_type>::from_promise(promise))
+ {}
+
+ /// Move constructor
+ AsyncGenerator(AsyncGenerator &&other) noexcept
+ : m_coroutine(other.m_coroutine) {
+ other.m_coroutine = nullptr;
+ }
+
+ /// Destructor.
+ ~AsyncGenerator() {
+ if (m_coroutine) {
+ m_coroutine.destroy();
+ }
+ }
+
+ /// Move-assignment operator.
+ AsyncGenerator& operator=(AsyncGenerator &&other) noexcept {
+ m_coroutine = other.m_coroutine;
+ other.m_coroutine = nullptr;
+ return *this;
+ }
+
+ /// Copy constructor
+ AsyncGenerator(const AsyncGenerator &) = delete;
+ /// Copy assignment operator
+ AsyncGenerator& operator=(const AsyncGenerator &) = delete;
+
+ /**
+ * \brief Returns an iterator-like awaitable
+ *
+ * The returned object is an awaitable that must be co_awaited to obtain
+ * an asynchronous iterator to iterate over results of the asychronous generator.
+ *
+ * The asynchronout iterator can be used like a regular iterator, except that incrementing
+ * the iterator returns an awaitable that must be co_awaited.
+ *
+ * If the generator coroutine throws an exception, it will be rethrown here.
+ */
+ auto begin() noexcept {
+ class BeginIteratorAwaitable final : public detail::IteratorAwaitableBase {
+ public:
+ explicit BeginIteratorAwaitable(std::nullptr_t) noexcept
+ : IteratorAwaitableBase(nullptr) {}
+ explicit BeginIteratorAwaitable(std::coroutine_handle<promise_type> producerCoroutine) noexcept
+ : IteratorAwaitableBase(producerCoroutine.promise(), producerCoroutine) {}
+
+ bool await_ready() const noexcept {
+ return m_promise == nullptr || IteratorAwaitableBase::await_ready();
+ }
+
+ iterator await_resume() {
+ if (m_promise == nullptr) {
+ return iterator{nullptr};
+ } else if (m_promise->finished()) {
+ m_promise->rethrow_if_unhandled_exception();
+ return iterator{nullptr};
+ }
+ return iterator{
+ std::coroutine_handle<promise_type>::from_promise(*static_cast<promise_type *>(m_promise))
+ };
+ }
+ };
+
+ return m_coroutine ? BeginIteratorAwaitable{m_coroutine} : BeginIteratorAwaitable{nullptr};
+ }
+
+ /// Returns an iterator representing the finished generator.
+ auto end() noexcept {
+ return iterator{nullptr};
+ }
+
+ void swap(AsyncGenerator &other) noexcept {
+ using std::swap;
+ swap(m_coroutine, other.m_coroutine);
+ }
+
+private:
+ std::coroutine_handle<promise_type> m_coroutine = {nullptr};
+};
+
+template<typename T>
+void swap(AsyncGenerator<T> &arg1, AsyncGenerator<T> &arg2) noexcept {
+ arg1.swap(arg2);
+}
+
+namespace detail {
+
+template<typename T>
+AsyncGenerator<T> AsyncGeneratorPromise<T>::get_return_object() noexcept {
+ return AsyncGenerator<T>{*this};
+}
+
+} // namespace detail
+
+} // namespace QCoro
+
+/**
+ * \brief Helper macro to asynchronously loop over values produced by the AsyncGenerator<T>.
+ *
+ * The macro is identical to Q_FOREACH, except that it makes sure to `co_await` on each
+ * iteration. It's identical to the following piece of code:
+ *
+ * @code
+ * for (auto it = co_await generator.begin(), end = generator.end(); it != end; co_await ++it) {
+ * var = *it;
+ * ...
+ * }
+ * @endcode
+ *
+ * @param var The full declaration of the iteration variable (incl. type, cvref), e.g. "const QString &user"
+ * @param generator The AsyncGenerator<T> object.
+ **/
+
+#define QCORO_FOREACH(var, generator) \
+ if (auto && _container = (generator); false) {} else \
+ for (auto _begin = co_await _container.begin(), _end = _container.end(); \
+ _begin != _end; \
+ co_await ++_begin) \
+ if (var = *_begin; false) {} else // NOLINT(bugprone-macro-parentheses)
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+namespace QCoro {
+
+template<typename T> class Task;
+template<typename T> class Generator;
+template<typename T> class GeneratorIterator;
+template<typename T> class AsyncGenerator;
+template<typename T> class AsyncGeneratorIterator;
+
+} // namespace QCoro
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <variant>
+#include <exception>
+
+#include "coroutine.h"
+
+#include <QDebug>
+
+namespace QCoro {
+
+template<typename T>
+class Generator;
+
+namespace detail {
+
+/**
+ * @brief Promise type for generator coroutine.
+ *
+ * GeneratorPromise is automatically constructed by the compiler
+ * (from Generator<T>::promise_type) inside the generator coroutine.
+ *
+ * The generator coroutine is suspended on start (it won't produce any value
+ * until asked for).
+ **/
+template<typename T>
+class GeneratorPromise {
+ using value_type = std::remove_reference_t<T>;
+public:
+ /**
+ * Constructs the Generator<T> object returned from the generator coroutine
+ * to the caller.
+ **/
+ Generator<T> get_return_object();
+
+ /**
+ * Indicates that the generator should suspend on start and
+ * only generate the first value when asked for.
+ **/
+ std::suspend_always initial_suspend() { return {}; }
+ /**
+ * Indicates that the generator coroutine should suspend when
+ * it reaches the end (or returns), rather then destroyed.
+ *
+ * The generator coroutine is destroyed only when the Generator<T>
+ * object is destroyed.
+ **/
+ std::suspend_always final_suspend() noexcept {
+ mValue = nullptr;
+ return {};
+ }
+
+ /**
+ * Called automatically when the generator coroutine throws an exception.
+ *
+ * The thrown exception is stored in the promise and will be re-thrown in the
+ * caller (see QCoro::detail::GeneratorIterator<T>::operator*()).
+ **/
+ void unhandled_exception() {
+ mException = std::current_exception();
+ }
+
+ /**
+ * Stores the current value produced (`co_yield`ed) by the generator coroutine.
+ *
+ * The value is stored and in the promise and the generator coroutine
+ * is suspended.
+ **/
+
+ std::suspend_always yield_value(value_type &value) {
+ mValue = std::addressof(value);
+ return {};
+ }
+
+ std::suspend_always yield_value(value_type &&value) {
+ mValue = std::addressof(value);
+ return {};
+ }
+
+ /**
+ * The generator coroutine itself must always be `void`.
+ **/
+ void return_void() {}
+
+ /**
+ * Returns the exception stored in the promise type (if any).
+ **/
+ std::exception_ptr exception() const {
+ return mException;
+ }
+
+ /**
+ * Returns the current value stored in the promise type.
+ **/
+ value_type &value() {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
+ return *const_cast<value_type *>(static_cast<const value_type *>(mValue));
+ }
+
+ /**
+ * Whether the generator coroutine has finished or not.
+ **/
+ bool finished() const {
+ return mValue == nullptr;
+ }
+
+ void rethrowIfException() {
+ if (mException) {
+ std::rethrow_exception(mException);
+ }
+ }
+
+ /**
+ * @brief Prevent use of `co_await` inside the generator coroutine
+ *
+ * Use `QCoro::AsyncGenerator<T>` if you need to use `co_await` inside the generator coroutine.
+ */
+ template<typename U>
+ std::suspend_never await_transform(U &&) = delete;
+
+private:
+ const void *mValue = nullptr;
+ std::exception_ptr mException;
+};
+
+} // namespace detail
+
+/**
+ * @brief Iterator to loop over values produced by the generator coroutine.
+ *
+ * Dereferencing the iterator will return a value produced by the generator coroutine.
+ * If the generator coroutine has thrown an exception, dereferencing the iterator will
+ * re-throw the exception.
+ *
+ * Incrementing the iterator will resume the generator coroutine, letting it run until
+ * it yields next value, or finishes. If the generator coroutine finishes, the iterator
+ * will become invalid and will be equal to Generator<T>::end().
+ *
+ * The iterator can only be obtained from Generator<T>::begin() and Generator<T>::end()
+ * methods.
+ */
+template<typename T>
+class GeneratorIterator {
+ using promise_type = detail::GeneratorPromise<T>;
+public:
+ using iterator_category = std::input_iterator_tag;
+ // Not sure what type should be used for difference_type as we don't
+ // allow calculating difference between two iterators.
+ using difference_type = std::ptrdiff_t;
+ using value_type = std::remove_reference_t<T>;
+ using reference = std::add_lvalue_reference_t<T>;
+ using pointer = std::add_pointer_t<value_type>;
+
+ /**
+ * @brief Resumes the generator coroutine until it yields new value or finishes.
+ *
+ * Returns an iterator holding the next value produced by the generator coroutine
+ * or an invalid iterator, indicating the generator coroutine has finishes.
+ *
+ * If the generator coroutine throws an exception, it will be rethrown from here.
+ **/
+ GeneratorIterator operator++() {
+ if (!mGeneratorCoroutine) {
+ return *this;
+ }
+
+ mGeneratorCoroutine.resume(); // generate next value
+ auto &promise = mGeneratorCoroutine.promise();
+ if (promise.finished()) {
+ mGeneratorCoroutine = nullptr;
+ promise.rethrowIfException();
+ }
+
+ return *this;
+ }
+
+ /**
+ * @brief Returns value produced by the generator coroutine.
+ **/
+ reference operator *() const noexcept {
+ return mGeneratorCoroutine.promise().value();
+ }
+
+ bool operator==(const GeneratorIterator &other) const noexcept {
+ return mGeneratorCoroutine == other.mGeneratorCoroutine;
+ }
+
+ bool operator!=(const GeneratorIterator &other) const noexcept {
+ return !(operator==(other));
+ }
+
+private:
+ friend class QCoro::Generator<T>;
+
+ /**
+ * @brief Constructs an invalid iterator.
+ **/
+ explicit GeneratorIterator(std::nullptr_t) {}
+ /**
+ * @brief Constructs an iterator associated with the given generator coroutine.
+ **/
+ explicit GeneratorIterator(std::coroutine_handle<promise_type> generatorCoroutine)
+ : mGeneratorCoroutine(generatorCoroutine)
+ {}
+
+ std::coroutine_handle<promise_type> mGeneratorCoroutine{nullptr};
+};
+
+/**
+ * @brief A coroutine generator.
+ *
+ * The Generator class is returned from the generator coroutine. It's similar
+ * to QCoro::Task<T> in the sense that it provides caller with an interface to
+ * the generator coroutine.
+ *
+ * There is no standard interface for generators defined in the C++ standard.
+ * Following the generators implemented in cppcoro (which is a big inspiration)
+ * for QCoro), the Generator<T> provides begin() method to obtain an iterator
+ * representing the current value produced by the generator. Each incrementation
+ * of the iterator resumes the generator and provides next value. The end() method
+ * can be used to obtain a past-end iterator to detect when the generator coroutine
+ * has finished (no more values will be produced), unless the generator is infinite.
+ *
+ * When the Generator<T> object is destroyed, the associated generator coroutine is
+ * also destroyed, even if it has not yet finished. All values allocated on stack of
+ * the generator coroutine will be destroyed automatically.
+ */
+template<typename T>
+class Generator {
+public:
+ using promise_type = detail::GeneratorPromise<T>;
+ using iterator = GeneratorIterator<T>;
+
+ explicit Generator() = default;
+ Generator(Generator &&other) noexcept {
+ mGeneratorCoroutine = other.mGeneratorCoroutine;
+ other.mGeneratorCoroutine = std::coroutine_handle<promise_type>::from_address(nullptr);
+ }
+ Generator(const Generator &) = delete;
+ Generator &operator=(Generator &&other) noexcept {
+ mGeneratorCoroutine = other.mGeneratorCoroutine;
+ other.mGeneratorCoroutine = std::coroutine_handle<promise_type>::from_address(nullptr);
+ return *this;
+ };
+ Generator &operator=(const Generator &) = delete;
+ /**
+ * @brief Destroys this Generator object and the associated generator coroutine.
+ *
+ * All values allocated on the generator stack are destroyed automatically.
+ */
+ ~Generator() {
+ if (mGeneratorCoroutine.address() != nullptr) {
+ mGeneratorCoroutine.destroy();
+ }
+ }
+
+ /**
+ * @brief Returns iterator "pointing" to the first value produced by the generator.
+ *
+ * If the generator coroutine did not produce any value and finished immediatelly,
+ * the returned iterator will be equal to end().
+ *
+ * If the generator coroutine has thrown an exception if will be rethrown from here.
+ **/
+ iterator begin() {
+ mGeneratorCoroutine.resume(); // generate first value
+ if (mGeneratorCoroutine.promise().finished()) { // did not yield anything
+ mGeneratorCoroutine.promise().rethrowIfException();
+ return iterator{nullptr};
+ }
+ return iterator{mGeneratorCoroutine};
+ }
+
+ /**
+ * @brief Returns iterator indicating the past-last value produced by the generator.
+ *
+ * Can be used to check whether the generator have produced another value or
+ * whether it has finished.
+ **/
+ iterator end() {
+ return iterator{nullptr};
+ }
+
+private:
+ friend QCoro::Generator<T> QCoro::detail::GeneratorPromise<T>::get_return_object();
+
+ /**
+ * @brief Constructs a new Generator object for given generator coroutine.
+ *
+ * This is called from GeneratorPromise<T>::get_return_object(), which is invoked
+ * automatically by C++ when starting the generator coroutine.
+ */
+ explicit Generator(std::coroutine_handle<promise_type> generatorCoroutine)
+ : mGeneratorCoroutine(generatorCoroutine)
+ {}
+
+ std::coroutine_handle<promise_type> mGeneratorCoroutine;
+};
+
+} // namespace QCoro
+
+template<typename T>
+QCoro::Generator<T> QCoro::detail::GeneratorPromise<T>::get_return_object() {
+ using handle_type = std::coroutine_handle<typename QCoro::Generator<T>::promise_type>;
+ return QCoro::Generator<T>(handle_type::from_promise(*this));
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+
+namespace QCoro {
+
+template<typename T>
+class LazyTask;
+
+/*! \cond internal */
+
+namespace detail {
+
+//! Specialization of QCoro::detail::isTask for LazyTask.
+template<typename T>
+struct isTask<QCoro::LazyTask<T>> : std::true_type {
+ using return_type = typename QCoro::LazyTask<T>::value_type;
+};
+
+template<typename T>
+class LazyTaskPromise : public TaskPromise<T>
+{
+public:
+ using TaskPromise<T>::TaskPromise;
+
+ LazyTask<T> get_return_object() noexcept;
+
+ std::suspend_always initial_suspend() const noexcept;
+};
+
+} // namespace detail
+
+/*! \endcond */
+
+template<typename T = void>
+class LazyTask final : public detail::TaskBase<T, LazyTask, detail::LazyTaskPromise<T>>
+{
+public:
+ using promise_type = detail::LazyTaskPromise<T>;
+ using value_type = T;
+
+ using detail::TaskBase<T, LazyTask, promise_type>::TaskBase;
+
+ ~LazyTask() override;
+
+ auto operator co_await() const noexcept;
+};
+
+} // namespace QCoro
+
+#include "impl/lazytask.h"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "coroutine.h"
+#include "concepts_p.h"
+
+#include <atomic>
+#include <exception>
+#include <variant>
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+namespace QCoro {
+
+template<typename T = void>
+class Task;
+
+/*! \cond internal */
+
+namespace detail {
+
+template<typename T>
+struct awaiter_type;
+
+template<typename T>
+using awaiter_type_t = typename awaiter_type<T>::type;
+
+//! Continuation that resumes a coroutine co_awaiting on currently finished coroutine.
+class TaskFinalSuspend {
+public:
+ //! Constructs the awaitable, passing it a handle to the co_awaiting coroutine
+ /*!
+ * \param[in] awaitingCoroutine handle of the coroutine that is co_awaiting the current
+ * coroutine (continuation).
+ */
+ explicit TaskFinalSuspend(const std::vector<std::coroutine_handle<>> &awaitingCoroutines);
+
+ //! Returns whether the just finishing coroutine should do final suspend or not
+ /*!
+ * If the coroutine is not being co_awaited by another coroutine, then don't
+ * suspend and let the code reach the end of the coroutine, which will take
+ * care of cleaning everything up. Otherwise we suspend and let the awaiting
+ * coroutine to clean up for us.
+ */
+ bool await_ready() const noexcept;
+
+ //! Called by the compiler when the just-finished coroutine is suspended. for the very last time.
+ /*!
+ * It is given handle of the coroutine that is being co_awaited (that is the current
+ * coroutine). If there is a co_awaiting coroutine and it has not been resumed yet,
+ * it resumes it.
+ *
+ * Finally, it destroys the just finished coroutine and frees all allocated resources.
+ *
+ * \param[in] finishedCoroutine handle of the just finished coroutine
+ */
+ template<typename Promise>
+ void await_suspend(std::coroutine_handle<Promise> finishedCoroutine) noexcept;
+
+ //! Called by the compiler when the just-finished coroutine should be resumed.
+ /*!
+ * In reality this should never be called, as our coroutine is done, so it won't be resumed.
+ * In any case, this method does nothing.
+ * */
+ constexpr void await_resume() const noexcept;
+
+private:
+ std::vector<std::coroutine_handle<>> mAwaitingCoroutines;
+};
+
+//! Base class for the \c Task<T> promise_type.
+/*!
+ * This is a promise_type for a Task<T> returned from a coroutine. When a coroutine
+ * is constructed, it looks at its return type, which will be \c Task<T>, and constructs
+ * new object of type \c Task<T>::promise_type (which will be \c TaskPromise<T>). Using
+ * \c TaskPromise<T>::get_return_object() it obtains a new object of Task<T>. Then the
+ * coroutine user code is executed and runs until the it reaches a suspend point - either
+ * a co_await keyword, co_return or until it reaches the end of user code.
+ *
+ * You can think about promise as an interface that is callee-facing, while Task<T> is
+ * an caller-facing interface (in respect to the current coroutine).
+ *
+ * Promise interface must provide several methods:
+ * * get_return_object() - it is called by the compiler at the very beginning of a coroutine
+ * and is used to obtain the object that will be returned from the coroutine whenever it is
+ * suspended.
+ * * initial_suspend() - it is co_awaited by the code generated immediately before user
+ * code. Depending on the Awaitable that it returns, the coroutine will either suspend
+ * and the user code will only be executed once it is co_awaited by some other coroutine,
+ * or it will begin executing the user code immediately. In case of QCoro, the promise always
+ * returns std::suspend_never, which is a standard library awaitable, which prevents the
+ * coroutine from being suspended at the beginning.
+ * * final_suspend() - it is co_awaited when the coroutine co_returns, or when it reaches
+ * the end of user code. Same as with initial_suspend(), depending on the type of Awaitable
+ * it returns, it either suspends the coroutine (and then it must be destroyed explicitly
+ * by the Awaiter), or resumes and takes care of destroying the frame pointer. In case of
+ * QCoro, the promise returns a custom Awaitable called TaskFinalSuspend, which, when
+ * co_awaited by the compiler-generated code will make sure that if there is a coroutine
+ * co_awaiting on the current corutine, that the co_awaiting coroutine is resumed.
+ * * unhandled_exception() - called by the compiler if the coroutine throws an unhandled
+ * exception. The promise will store the exception and it will be rethrown when the
+ * co_awaiting coroutine tries to retrieve a result of the coroutine that has thrown.
+ * * return_value() - called by the compiler to store co_returned result of the function.
+ * It must only be present if the coroutine is not void.
+ * * return_void() - called by the compiler when the coroutine co_returns or flows of the
+ * end of user code. It must only be present if the coroutine return type is void.
+ * * await_transform() - this one is optional and is used by co_awaits inside the coroutine.
+ * It allows the promise to transform the co_awaited type to an Awaitable.
+ */
+class TaskPromiseBase {
+public:
+ //! Called when the coroutine is started to decide whether it should be suspended or not.
+ /*!
+ * We want coroutines that return QCoro::Task<T> to start automatically, because it will
+ * likely be executed from Qt's event loop, which will not co_await it, but rather call
+ * it as a regular function, therefore it returns `std::suspend_never` awaitable, which
+ * indicates that the coroutine should not be suspended.
+ * */
+ std::suspend_never initial_suspend() const noexcept;
+
+ //! Called when the coroutine co_returns or reaches the end of user code.
+ /*!
+ * This decides what should happen when the coroutine is finished.
+ */
+ auto final_suspend() const noexcept;
+
+ //! Called by co_await to obtain an Awaitable for type \c T.
+ /*!
+ * When co_awaiting on a value of type \c T, the type \c T must an Awaitable. To allow
+ * to co_await even on types that are not Awaitable (e.g. 3rd party types like QNetworkReply),
+ * C++ allows promise_type to provide \c await_transform() function that can transform
+ * the type \c T into an Awaitable. This is a very powerful mechanism in C++ coroutines.
+ *
+ * For types \c T for which there is no valid await_transform() overload, the C++ attempts
+ * to use those types directly as Awaitables. This is a perfectly valid scenario in cases
+ * when co_awaiting a type that implements the neccessary Awaitable interface.
+ *
+ * In our implementation, the await_transform() is overloaded only for Qt types for which
+ * a specialiation of the \c QCoro::detail::awaiter_type template class exists. The
+ * specialization returns type of the Awaiter for the given type \c T.
+ */
+ template<typename T, typename Awaiter = QCoro::detail::awaiter_type_t<std::remove_cvref_t<T>>>
+ auto await_transform(T &&value);
+
+ //! If the type T is already an awaitable (including Task or LazyTask), then just forward it as it is.
+ template<Awaitable T>
+ auto && await_transform(T &&awaitable);
+
+ //! \copydoc template<Awaitable T> QCoro::TaskPromiseBase::await_transform(T &&)
+ template<Awaitable T>
+ auto &await_transform(T &awaitable);
+
+ //! Called by \c TaskAwaiter when co_awaited.
+ /*!
+ * This function is called by a TaskAwaiter, e.g. an object obtain by co_await
+ * when a value of Task<T> is co_awaited (in other words, when a coroutine co_awaits on
+ * another coroutine returning Task<T>).
+ *
+ * \param awaitingCoroutine handle for the coroutine that is co_awaiting on a coroutine that
+ * represented by this promise. When our coroutine finishes, it's
+ * our job to resume the awaiting coroutine.
+ */
+ void addAwaitingCoroutine(std::coroutine_handle<> awaitingCoroutine);
+
+ bool hasAwaitingCoroutine() const;
+
+ void derefCoroutine();
+ void refCoroutine();
+ void destroyCoroutine();
+
+protected:
+ explicit TaskPromiseBase();
+
+private:
+ friend class TaskFinalSuspend;
+
+ //! Handle of the coroutine that is currently co_awaiting this Awaitable
+ std::vector<std::coroutine_handle<>> mAwaitingCoroutines;
+
+ //! Indicates whether we can destroy the coroutine handle
+ std::atomic<uint32_t> mRefCount{0};
+};
+
+//! The promise_type for Task<T>
+/*!
+ * See \ref TaskPromiseBase documentation for explanation about promise_type.
+ */
+template<typename T>
+class TaskPromise: public TaskPromiseBase {
+public:
+ explicit TaskPromise() = default;
+ ~TaskPromise() = default;
+
+ //! Constructs a Task<T> for this promise.
+ Task<T> get_return_object() noexcept;
+
+ //! Called by the compiler when user code throws an unhandled exception.
+ /*!
+ * When user code throws but doesn't catch, it is ultimately caught by the code generated by
+ * the compiler (effectively the entire user code is wrapped in try...catch ) and this method
+ * is called. This method stores the exception. The exception is re-thrown when the calling
+ * coroutine is resumed and tries to retrieve result of the finished coroutine that has thrown.
+ */
+ void unhandled_exception();
+
+ //! Called form co_return statement to store result of the coroutine.
+ /*!
+ * \param[in] value the value returned by the coroutine. It is stored in the
+ * promise, later can be retrieved by the calling coroutine.
+ */
+ void return_value(T &&value) noexcept;
+
+ //! \copydoc template<typename T> TaskPromise::return_value(T &&value) noexcept
+ void return_value(const T &value) noexcept;
+
+ template<typename U> requires QCoro::concepts::constructible_from<T, U>
+ void return_value(U &&value) noexcept;
+
+ //! Retrieves the result of the coroutine.
+ /*!
+ * \return the value co_returned by the finished coroutine. If the coroutine has
+ * thrown an exception, this method will instead rethrow the exception.
+ */
+ T &result() &;
+ //! \copydoc T &QCoro::TaskPromise<T>::result() &
+ T &&result() &&;
+
+private:
+ //! Holds either the return value of the coroutine or exception thrown by the coroutine.
+ std::variant<std::monostate, T, std::exception_ptr> mValue;
+};
+
+//! Specialization of TaskPromise for coroutines returning \c void.
+template<>
+class TaskPromise<void>: public TaskPromiseBase {
+public:
+ // Constructor.
+ explicit TaskPromise() = default;
+
+ //! Destructor.
+ ~TaskPromise() = default;
+
+ //! \copydoc TaskPromise<T>::get_return_object()
+ Task<void> get_return_object() noexcept;
+
+ //! \copydoc TaskPromise<T>::unhandled_exception()
+ void unhandled_exception();
+
+ //! Promise type must have this function when the coroutine return type is void.
+ void return_void() noexcept;
+
+ //! Provides access to the result of the coroutine.
+ /*!
+ * Since this is a promise type for a void coroutine, the only result that
+ * this can return is re-throwing an exception thrown by the coroutine, if
+ * there's any.
+ */
+ void result();
+
+private:
+ //! Exception thrown by the coroutine.
+ std::exception_ptr mException;
+};
+
+//! Base-class for Awaiter objects returned by the \c Task<T> operator co_await().
+template<typename Promise>
+class TaskAwaiterBase {
+public:
+ //! Returns whether to co_await
+ bool await_ready() const noexcept;
+
+ //! Called by co_await in a coroutine that co_awaits our awaited coroutine managed by the current task.
+ /*!
+ * In other words, let's have a case like this:
+ * \code{.cpp}
+ * Task<> doSomething() {
+ * ...
+ * co_return result;
+ * };
+ *
+ * Task<> getSomething() {
+ * ...
+ * const auto something = co_await doSomething();
+ * ...
+ * }
+ * \endcode
+ *
+ * If this Awaiter object is an awaiter of the doSomething() coroutine (e.g. has been constructed
+ * by the co_await), then \c mAwaitedCoroutine is the handle of the doSomething() coroutine,
+ * and \c awaitingCoroutine is a handle of the getSomething() coroutine which is awaiting the
+ * completion of the doSomething() coroutine.
+ *
+ * This is implemented by passing the awaiting coroutine handle to the promise of the
+ * awaited coroutine. When the awaited coroutine finishes, the promise will take care of
+ * resuming the awaiting coroutine. At the same time this function resumes the awaited
+ * coroutine.
+ *
+ * \param[in] awaitingCoroutine handle of the coroutine that is currently co_awaiting the
+ * coroutine represented by this Tak.
+ * \return returns whether the awaiting coroutine should be suspended, or whether the
+ * co_awaited coroutine has finished synchronously and the co_awaiting coroutine doesn't
+ * have to suspend.
+ */
+ void await_suspend(std::coroutine_handle<> awaitingCoroutine) noexcept;
+
+protected:
+ //! Constucts a new Awaiter object.
+ /*!
+ * \param[in] coroutine hande for the coroutine that is being co_awaited.
+ */
+ explicit TaskAwaiterBase(std::coroutine_handle<Promise> awaitedCoroutine);
+
+ //! Handle of the coroutine that is being co_awaited by this awaiter
+ std::coroutine_handle<Promise> mAwaitedCoroutine = {};
+};
+
+template<typename T>
+struct isTask : std::false_type {
+ using return_type = T;
+};
+
+template<typename T>
+struct isTask<QCoro::Task<T>> : std::true_type {
+ using return_type = typename QCoro::Task<T>::value_type;
+};
+
+template<typename T>
+constexpr bool isTask_v = isTask<T>::value;
+
+
+template<typename T, template<typename> class TaskImpl, typename PromiseType>
+class TaskBase {
+public:
+ explicit TaskBase() noexcept = default;
+
+ explicit TaskBase(std::coroutine_handle<PromiseType> coroutine);
+
+ //! Task cannot be copy-constructed.
+ TaskBase(const TaskBase &) = delete;
+ //! Task cannot be copy-assigned.
+ TaskBase &operator=(const TaskBase &) = delete;
+
+ //! The task can be move-constructed.
+ TaskBase(TaskBase &&otbher) noexcept;
+
+ //! The task can be move-assigned.
+ TaskBase &operator=(TaskBase &&other) noexcept;
+
+ //! Destructor.
+ virtual ~TaskBase();
+
+ //! Returns whether the task has finished.
+ /*!
+ * A task that is ready (represents a finished coroutine) must not attempt
+ * to suspend the coroutine again.
+ */
+ bool isReady() const;
+
+ //! Provides an Awaiter for the coroutine machinery.
+ /*!
+ * The coroutine machinery looks for a suitable operator co_await overload
+ * for the current Awaitable (this Task). It calls it to obtain an Awaiter
+ * object, that is an object that the co_await keyword uses to suspend and
+ * resume the coroutine.
+ */
+ auto operator co_await() const noexcept;
+
+ //! A callback to be invoked when the asynchronous task finishes.
+ /*!
+ * In some scenarios it is not possible to co_await a coroutine (for example from
+ * a third-party code that cannot be changed to be a coroutine). In that case,
+ * chaining a then() callback is a possible solution how to handle a result
+ * of a coroutine without co_awaiting it.
+ *
+ * @param callback A function or a function object that can be invoked without arguments
+ * (discarding the result of the coroutine) or with a single argument of type T that is
+ * type matching the return type of the coroutine or is implicitly constructible from
+ * the return type returned by the coroutine.
+ *
+ * @return Returns Task<R> where R is the return type of the callback, so that the
+ * result of the then() action can be co_awaited, if desired. If the callback
+ * returns an awaitable (Task<R>) then the result of then is the awaitable.
+ */
+ template<typename ThenCallback>
+ requires (std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>))
+ auto then(ThenCallback &&callback) &;
+ template<typename ThenCallback>
+ requires (std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>))
+ auto then(ThenCallback &&callback) &&;
+
+
+ template<typename ThenCallback, typename ErrorCallback>
+ requires ((std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>)) &&
+ std::is_invocable_v<ErrorCallback, const std::exception &>)
+ auto then(ThenCallback &&callback, ErrorCallback &&errorCallback) &;
+ template<typename ThenCallback, typename ErrorCallback>
+ requires ((std::is_invocable_v<ThenCallback> || (!std::is_void_v<T> && std::is_invocable_v<ThenCallback, T>)) &&
+ std::is_invocable_v<ErrorCallback, const std::exception &>)
+ auto then(ThenCallback &&callback, ErrorCallback &&errorCallback) &&;
+
+private:
+ template<typename ThenCallback, typename ... Args>
+ static auto invokeCb(ThenCallback &&callback, [[maybe_unused]] Args && ... args);
+
+ template<typename ThenCallback, typename Arg>
+ struct cb_invoke_result: std::conditional_t<
+ std::is_invocable_v<ThenCallback>,
+ std::invoke_result<ThenCallback>,
+ std::invoke_result<ThenCallback, Arg>
+ > {};
+
+ template<typename ThenCallback>
+ struct cb_invoke_result<ThenCallback, void>: std::invoke_result<ThenCallback> {};
+
+ template<typename ThenCallback, typename Arg>
+ using cb_invoke_result_t = typename cb_invoke_result<ThenCallback, Arg>::type;
+
+ template<typename R, typename ErrorCallback,
+ typename U = typename detail::isTask<R>::return_type>
+ static auto handleException(ErrorCallback &errCb, const std::exception &exception) -> U;
+
+ template<typename TaskT, typename ThenCallback, typename ErrorCallback, typename R = cb_invoke_result_t<ThenCallback, T>>
+ static auto thenImpl(TaskT task, ThenCallback &&thenCallback, ErrorCallback &&errorCallback) -> std::conditional_t<detail::isTask_v<R>, R, TaskImpl<R>>;
+ template<typename TaskT, typename ThenCallback, typename ErrorCallback, typename R = cb_invoke_result_t<ThenCallback, T>>
+ static auto thenImplRef(TaskT &task, ThenCallback &&thenCallback, ErrorCallback &&errorCallback) -> std::conditional_t<detail::isTask_v<R>, R, TaskImpl<R>>;
+
+protected:
+ std::coroutine_handle<PromiseType> mCoroutine = {};
+};
+
+
+
+} // namespace detail
+
+/*! \endcond */
+
+//! An asynchronously executed task.
+/*!
+ * When a coroutine is called which has return type Task<T>, the coroutine will
+ * construct a new instance of Task<T> which will be returned to the caller when
+ * the coroutine is suspended - that is either when it co_awaits another coroutine
+ * of finishes executing user code.
+ *
+ * In the sense of the interface that the task implements, it is an Awaitable.
+ *
+ * Task<T> is constructed by a code generated at the beginning of the coroutine by
+ * the compiler (i.e. before the user code). The code first creates a new frame
+ * pointer, which internally holds the promise. The promise is of type \c R::promise_type,
+ * where \c R is the return type of the function (so \c Task<T>, in our case.
+ *
+ * One can think about it as Task being the caller-facing interface and promise being
+ * the callee-facing interface.
+ */
+template<typename T>
+class Task final : public detail::TaskBase<T, Task, detail::TaskPromise<T>> {
+public:
+ //! Promise type of the coroutine. This is required by the C++ standard.
+ using promise_type = detail::TaskPromise<T>;
+ //! The type of the coroutine return value.
+ using value_type = T;
+
+ using detail::TaskBase<T, Task, detail::TaskPromise<T>>::TaskBase;
+};
+
+namespace detail
+{
+
+template <typename T>
+concept TaskConvertible = requires(T val, TaskPromiseBase promise)
+{
+ { promise.await_transform(val) };
+};
+
+template<typename T>
+struct awaitable_return_type {
+ using type = std::decay_t<decltype(std::declval<T>().await_resume())>;
+};
+
+template<QCoro::detail::has_member_operator_coawait T>
+struct awaitable_return_type<T> {
+ using type = std::decay_t<typename awaitable_return_type<decltype(std::declval<T>().operator co_await())>::type>;
+};
+
+template<QCoro::detail::has_nonmember_operator_coawait T>
+struct awaitable_return_type<T> {
+ using type = std::decay_t<typename awaitable_return_type<decltype(operator co_await(std::declval<T>()))>::type>;
+};
+
+template<Awaitable Awaitable>
+using awaitable_return_type_t = typename detail::awaitable_return_type<Awaitable>::type;
+
+template <typename Awaitable>
+requires TaskConvertible<Awaitable>
+using convertible_awaitable_return_type_t = typename detail::awaitable_return_type<decltype(std::declval<TaskPromiseBase>().await_transform(Awaitable()))>::type;
+
+} // namespace detail
+
+//! Waits for a coroutine to complete in a blocking manner.
+/*!
+ * Sometimes you may need to wait for a coroutine to finish without co_awaiting it - that is,
+ * you want to wait for the coroutine in a blocking mode. This function does exactly that.
+ * The function creates a nested QEventLoop and executes it until the coroutine has finished.
+ *
+ * \param task Coroutine to blockingly wait for.
+ * \returns Result of the coroutine.
+ */
+template<typename T>
+inline T waitFor(QCoro::Task<T> &task);
+
+// \overload
+template<typename T>
+inline T waitFor(QCoro::Task<T> &&task);
+
+// \overload
+template<Awaitable Awaitable>
+inline auto waitFor(Awaitable &&awaitable);
+
+//! Connect a callback to be called when the asynchronous task finishes.
+/*!
+ * Allows to register a callback to be called only when the context object
+ * still exists when the task finishes.
+ *
+ * In contrast to the then function, the result of connect can not be co_awaited.
+ *
+ * @param context A QObject that still needs to exist when the task finishes in order for the callback to be invoked.
+ * @param func The function that will be called when the task finishes.
+ * For void tasks, it needs to have no arguments.
+ * For all other types, it takes a value of the type as single argument.
+ */
+template <typename T, typename QObjectSubclass, typename Callback>
+requires std::is_invocable_v<Callback> || std::is_invocable_v<Callback, T> || std::is_invocable_v<Callback, QObjectSubclass *> || std::is_invocable_v<Callback, QObjectSubclass *, T>
+void connect(QCoro::Task<T> &&task, QObjectSubclass *context, Callback func);
+
+template <typename T, typename QObjectSubclass, typename Callback>
+requires detail::TaskConvertible<T>
+ && (std::is_invocable_v<Callback> || std::is_invocable_v<Callback, detail::convertible_awaitable_return_type_t<T>> || std::is_invocable_v<Callback, QObjectSubclass *> || std::is_invocable_v<Callback, QObjectSubclass *, detail::convertible_awaitable_return_type_t<T>>)
+ && (!detail::isTask_v<T>)
+void connect(T &&future, QObjectSubclass *context, Callback func);
+
+} // namespace QCoro
+
+#include "impl/taskfinalsuspend.h"
+#include "impl/taskpromisebase.h"
+#include "impl/taskpromise.h"
+#include "impl/taskawaiterbase.h"
+#include "impl/taskbase.h"
+#include "impl/task.h"
+#include "impl/waitfor.h"
+#include "impl/connect.h"
--- /dev/null
+# SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+# SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME Qml
+ INCLUDEDIR Qml
+ SOURCES
+ qcoroqmltask.cpp
+ qcoroqml.cpp
+ CAMELCASE_HEADERS
+ QCoroQmlTask
+ QCoroQml
+ QT_LINK_LIBRARIES
+ PUBLIC Core Qml
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro
+)
+
+if (${QT_VERSION_MAJOR} STREQUAL "6")
+ target_link_libraries(QCoro${QT_VERSION_MAJOR}Qml PRIVATE Qt::QmlPrivate)
+endif()
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroqml.h"
+
+#include "qcoroqmltask.h"
+
+#include <QQmlApplicationEngine>
+
+void QCoro::Qml::registerTypes() {
+ qRegisterMetaType<QCoro::QmlTask>();
+ qmlRegisterAnonymousType<QCoro::QmlTaskListener>("QCoro", 0);
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcoroqml_export.h"
+
+#include "qcoroqmltask.h"
+
+namespace QCoro::Qml {
+
+QCOROQML_EXPORT void registerTypes();
+
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroqmltask.h"
+
+#include <QLoggingCategory>
+
+#include <optional>
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+QT_WARNING_PUSH
+QT_WARNING_DISABLE_MSVC(4458 4201)
+#include <private/qjsvalue_p.h>
+QT_WARNING_POP
+#endif
+
+Q_DECLARE_LOGGING_CATEGORY(qcoroqml)
+Q_LOGGING_CATEGORY(qcoroqml, "qcoro.qml")
+
+namespace QCoro {
+
+struct QmlTaskPrivate : public QSharedData {
+ std::optional<QCoro::Task<QVariant>> task;
+
+ QmlTaskPrivate() = default;
+ QmlTaskPrivate(const QmlTaskPrivate &) : QSharedData() {
+ Q_UNREACHABLE();
+ };
+};
+
+inline QJSEngine *getEngineForValue(const QJSValue &val) {
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ // QJSValue::engine is deprecated, but still nicer, since it doesn't require using private symbols
+ QT_WARNING_PUSH
+ QT_WARNING_DISABLE_DEPRECATED
+ return val.engine();
+ QT_WARNING_POP
+#else
+ return QJSValuePrivate::engine(&val)->jsEngine();
+#endif
+}
+
+QmlTask::QmlTask() noexcept : d(new QmlTaskPrivate())
+{
+}
+
+QmlTask::QmlTask(QCoro::Task<QVariant> &&task)
+ : d(new QmlTaskPrivate())
+{
+ d->task = std::move(task);
+}
+
+QmlTask &QmlTask::operator=(const QmlTask &other) = default;
+QmlTask::QmlTask(const QmlTask &other) = default;
+QmlTask::~QmlTask() = default;
+
+void QmlTask::then(QJSValue func) {
+ if (!d->task) {
+ qCWarning(qcoroqml, ".then called on a QmlTask that is not connected to any coroutine. "
+ "Make sure you don't default-construct QmlTask in your code");
+ return;
+ }
+
+ if (!func.isCallable()) {
+ qCWarning(qcoroqml, ".then called with an argument that is not a function. The .then call will do nothing");
+ return;
+ }
+
+ d->task->then([func = std::move(func)](const QVariant &result) mutable -> void {
+ auto jsval = getEngineForValue(func)->toScriptValue(result);
+ func.call({jsval});
+ });
+}
+
+QmlTaskListener *QmlTask::await(const QVariant &intermediateValue)
+{
+ QPointer listener = new QmlTaskListener();
+ if (!intermediateValue.isNull()) {
+ listener->setValue(QVariant(intermediateValue));
+ }
+ d->task->then([listener](auto &&value) {
+ if (listener) {
+ listener->setValue(std::move(value));
+ }
+ });
+ return listener;
+}
+
+QVariant QmlTaskListener::value() const
+{
+ return m_value;
+}
+
+void QmlTaskListener::setValue(QVariant &&value)
+{
+ m_value = std::move(value);
+ Q_EMIT valueChanged();
+}
+
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QCoro/QCoroTask>
+#include <QJSValue>
+#include <QJSEngine>
+
+#include "qcoroqml_export.h"
+
+#include <type_traits>
+
+
+namespace QCoro {
+
+struct QmlTaskPrivate;
+
+class QmlTaskListener;
+
+//! QML type that allows to react to asynchronous computations from QML
+struct QCOROQML_EXPORT QmlTask {
+ Q_GADGET
+
+public:
+ // Just for Q_DECLARE_METATYPE to be happy
+ explicit QmlTask() noexcept;
+ QmlTask(const QmlTask &other);
+ QmlTask &operator=(const QmlTask &other);
+ ~QmlTask();
+
+ //! Constructs a QmlTask from a QCoro::Task that resolves to a QVariant
+ /*!
+ * \param[in] task to await
+ */
+ QmlTask(QCoro::Task<QVariant> &&task);
+
+ //! Constructs a QmlTask from a QCoro::Task that resolves to an arbitrary type
+ /*!
+ * \param[in] task to await
+ */
+ template <typename T>
+ QmlTask(QCoro::Task<T> &&task) : QmlTask(
+ task.then([](T &&result) -> QCoro::Task<QVariant> {
+ co_return QVariant::fromValue(std::forward<T>(result));
+ }))
+ {
+ // To rely on Qt's assertion to check whether the type is a registered metatype
+ qMetaTypeId<T>();
+ }
+
+ //! Constructs a QmlTask from any awaitable type supported by QCoro.
+ /*!
+ * \param[in] object to await
+ */
+ template <typename T>
+ requires (detail::TaskConvertible<T> && !std::is_same_v<T, QmlTask>)
+ QmlTask(T &&future) : QmlTask(detail::toTask(std::forward<T>(future)))
+ {
+ }
+
+ //! Constructs a QmlTask from a QCoro::Task that doesn't return a value
+ /*!
+ * \param[in] task to await
+ */
+ template <typename T = void>
+ QmlTask(QCoro::Task<> &&task) : QmlTask(
+ task.then([]() -> QCoro::Task<QVariant> {
+ co_return QVariant();
+ }))
+ {
+ }
+
+ //! QML function that executes a given JavaScript function once the computation is finished
+ /*!
+ * \param[in] JavaScript function to call once the result is ready.
+ * The result will be passed as first argument to the given function
+ */
+ Q_INVOKABLE void then(QJSValue func);
+
+ //! Allows to use tasks in property bindings
+ /*!
+ * The value property is initially null, and will be updated to the actual value once it is available.
+ * Example usage:
+ * ```
+ * text: asyncLoadText().await().value
+ * ```
+ *
+ * Optionally, an intermediate value can be passed to await,
+ * which will be returned by value while calculating the asynchronous result.
+ */
+ Q_INVOKABLE QCoro::QmlTaskListener *await(const QVariant &intermediateValue = {});
+
+private:
+ QSharedDataPointer<QmlTaskPrivate> d;
+};
+
+class QmlTaskListener : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QVariant value READ value NOTIFY valueChanged)
+
+public:
+ QVariant value() const;
+ void setValue(QVariant &&value);
+ Q_SIGNAL void valueChanged();
+
+private:
+ QVariant m_value;
+};
+
+}
+
+Q_DECLARE_METATYPE(QCoro::QmlTask)
--- /dev/null
+# SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+# SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME Quick
+ SOURCES
+ qcoroimageprovider.cpp
+ CAMELCASE_HEADERS
+ QCoroImageProvider
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro Core
+ QT_LINK_LIBRARIES
+ PUBLIC Core Gui Quick
+)
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroimageprovider.h"
+
+namespace QCoro {
+
+// Internal implementation of the QQuickImageResponse interface
+class QCoroImageResponse : public QQuickImageResponse {
+
+public:
+ QQuickTextureFactory *textureFactory() const override;
+ void reportFinished(QImage &&image);
+
+private:
+ QImage m_image;
+};
+
+
+ImageProvider::ImageProvider() = default;
+
+QQuickImageResponse *ImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) {
+ auto task = asyncRequestImage(id, requestedSize);
+
+ auto *response = new QCoroImageResponse();
+
+ task.then([response](QImage &&image) {
+ response->reportFinished(std::move(image));
+ });
+
+ return response;
+}
+
+QQuickTextureFactory *QCoroImageResponse::textureFactory() const {
+ return QQuickTextureFactory::textureFactoryForImage(m_image);
+}
+
+void QCoroImageResponse::reportFinished(QImage &&image) {
+ m_image = image;
+ Q_EMIT finished();
+}
+
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QQuickAsyncImageProvider>
+
+#include <QCoro/QCoroTask>
+
+#include "qcoroquick_export.h"
+
+namespace QCoro {
+
+//! Base class for coroutines based image providers
+class QCOROQUICK_EXPORT ImageProvider : public QQuickAsyncImageProvider {
+public:
+ explicit ImageProvider();
+
+ //! This function needs to be re-implemented in a subclass.
+ virtual QCoro::Task<QImage> asyncRequestImage(const QString &id, const QSize &requestedSize) = 0;
+
+private:
+ QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
+};
+
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifndef QCORO_NO_WARN_DEPRECATED_TASK_H
+ #if defined(__GNUC__) || defined(__clang__)
+ #pragma message "The qcoro/task.h include header is deprecated and will be removed " \
+ "in some future version of QCoro. Please use qcorotask.h header instead. " \
+ "You can define QCORO_NO_WARN_DEPRECATED_TASK_H to suppress this warning."
+ #elif defined(_MSC_VER)
+ #pragma message( "The qcoro/task.h include header is deprecated and will be removed " \
+ "in some future version of QCoro. Please use qcorotask.h header instead. " \
+ "You can define QCORO_NO_WARN_DEPRECATED_TASK_H to suppress this warning." )
+ #endif
+#endif
+
+#include "qcorotask.h"
--- /dev/null
+# SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME Test
+ INTERFACE
+ CAMELCASE_HEADERS
+ QCoroTest
+ QT_LINK_LIBRARIES
+ INTERFACE Test
+)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+/*
+ * This code is very closely based on qtestcase.h header file from Qt
+ * Copyright (c) 2016 The Qt Company Ltd., LGPLv3/GPLv2 or GPLv3 or any
+ * later version approved by the KDE Free Qt Foundation.
+ */
+
+#pragma once
+
+#include <qtestcase.h>
+#include <QString>
+
+/**
+ * @brief Coroutine-friendly version of QVERIFY test macro.
+ **/
+#define QCORO_VERIFY(statement) \
+ do { \
+ if (!QTest::qVerify(static_cast<bool>(statement), #statement, "", __FILE__, __LINE__)) \
+ co_return; \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QFAIL test macro.
+ **/
+#define QCORO_FAIL(message) \
+ do { \
+ QTest::qFail(static_cast<const char *>(message), __FILE__, __LINE__); \
+ co_return; \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QVERIFY2 test macro.
+ **/
+#define QCORO_VERIFY2(statement, description) \
+ do { \
+ if (statement) { \
+ if (!QTest::qVerify(true, #statement, static_cast<const char *>(description), __FILE__, __LINE__)) \
+ co_return; \
+ } else { \
+ if (!QTest::qVerify(false, #statement, static_cast<const char *>(description), __FILE__, __LINE__)) \
+ co_return; \
+ } \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QCOMPARE test macro.
+ **/
+#define QCORO_COMPARE(actual, expected) \
+ do { \
+ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
+ co_return; \
+ } while (false)
+
+
+#if !defined(QT_NO_EXCEPTIONS)
+
+/**
+ * @brief Coroutine-friendly version of QVERIFY_THROWS_EXCEPTION macro.
+ **/
+#define QCORO_VERIFY_THROWS_EXCEPTION(exceptionType, ...) \
+ do { \
+ try { \
+ try { \
+ __VA_ARGS__; \
+ QTest::qFail("Expected exception of type " #exceptionType " to be thrown" \
+ " but no exception caught", \
+ __FILE__, __LINE__); \
+ co_return; \
+ } catch (const exceptionType &) {} \
+ } catch (const std::exception &e) { \
+ const QByteArray msg = QByteArray() + \
+ "Expected exception of type " #exceptionType \
+ " to be thrown, but std::exception caught with message " + \
+ e.what(); \
+ QTest::qFail(msg.constData(), __FILE__, __LINE__); \
+ co_return; \
+ } catch (...) { \
+ QTest::qFail("Expected exception of type " #exceptionType " to be thrown" \
+ " but unknown exception caught", \
+ __FILE__, __LINE__); \
+ co_return; \
+ } \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QVERIFY_EXCEPTION_THROWN macro.
+ **/
+#define QCORO_VERIFY_EXCEPTION_THROWN(expression, exceptionType) \
+ QCORO_VERIFY_THROWS_EXCEPTION(exceptionType, expression)
+
+#else // QT_NO_EXCEPTIONS
+
+#define QCORO_VERIFY_EXCEPTION_THROWN(expression, exceptionType) \
+ static_assert(false, "Support of exceptions is disabled")
+#define QCORO_VERIFY_THROWS_EXCEPTION(exceptionType, ...) \
+ static_assert(false, "Support for exceptions is disabled")
+
+#endif // QT_NO_EXCEPTIONS
+
+/* @cond INTERNAL */
+
+#define QCORO_TRY_TIMEOUT_DEBUG_IMPL(expr, timeoutValue, step) \
+ if (!(expr)) { \
+ QTRY_LOOP_IMPL((expr), (2 * timeoutValue), step); \
+ if (expr) { \
+ QString msg = QString::fromUtf8("QTestLib: This test case check (\"%1\") failed because the requested timeout (%2 ms) was too short, %3 ms would have been sufficient this time."); \
+ msg = msg.arg(QString::fromUtf8(#expr)).arg(timeoutValue).arg(timeoutValue + qt_test_i); \
+ QCORO_FAIL(qPrintable(msg)); \
+ } \
+ }
+
+#define QCORO_TRY_IMPL(expr, timeout) \
+ const int qcoro_test_step = timeout < 350 ? timeout / 7 + 1 : 50; \
+ const int qcoro_test_timeoutValue = timeout; \
+ { QCORO_TRY_LOOP_IMPL((expr), qcoro_test_timeoutValue, qcoro_test_step); } \
+ QCORO_TRY_TIMEOUT_DEBUG_IMPL((expr), qcoro_test_timeoutValue, qcoro_test_step) \
+
+/* @endcond */
+
+/**
+ * @brief Coroutine-friendly version of QVERIFY_WITH_TIMEOUT test macro.
+ **/
+#define QCORO_TRY_VERIFY_WITH_TIMEOUT(expr, timeout) \
+ do { \
+ QCORO_TRY_IMPL((expr), timeout); \
+ QCORO_VERIFY(expr); \
+ } while (false);
+
+/**
+ * @brief Coroutine-friendly version of QTRY_VERIFY test macro.
+ **/
+#define QCORO_TRY_VERIFY(expr) QCORO_TRY_VERIFY_WITH_TIMEOUT((expr), 5000)
+
+/**
+ * @brief Coroutine-friendly version of QTRY_VERIFY2_WITH_TIMEOUT test macro.
+ **/
+#define QCORO_TRY_VERIFY2_WITH_TIMEOUT(expr, messageExpression, timeout) \
+ do { \
+ QCORO_TRY_IMPL((expr), timeout); \
+ QCORO_VERIFY2((expr), messageExpression); \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QTRY_VERIFY2 test macro.
+ **/
+#define QCORO_TRY_VERIFY2(expr, messageExpression) \
+ QCORO_TRY_VERIFY2_WITH_TIMEOUT((expr), (messageExpression), 5000)
+
+/**
+ * @brief Coroutine-friendly version of QTRY_COMPARE_WITH_TIMEOUT test macro.
+ **/
+#define QCORO_TRY_COMPARE_WITH_TIMEOUT(expr, expected, timeout) \
+ do { \
+ QCORO_TRY_IMPL(((expr) == (expected)), timeout); \
+ QCORO_COMPARE((expr), expected); \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QTRY_COMPARE test macro.
+ **/
+#define QCORO_TRY_COMPARE(expr, expected) \
+ QCORO_TRY_COMPARE_WITH_TIMEOUT((expr), (expected), 5000)
+
+/**
+ * @internal
+ **/
+#define QCORO_SKIP_INTERNAL(statement) \
+ do { \
+ QTest::qSkip(static_cast<const char *>(statement), __FILE__, __LINE__); \
+ co_return; \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QSKIP test macro.
+ **/
+#define QCORO_SKIP(statement, ...) QCORO_SKIP_INTERNAL(statement)
+
+/**
+ * @brief Coroutine-friendly version of QEXPECT_FAIL test macro.
+ **/
+#define QCORO_EXPECT_FAIL(dataIndex, comment, mode) \
+ do { \
+ if (!QTest::qExpectFail(dataIndex, static_cast<const char *>(comment), QTest::mode, __FILE__, __LINE__)) \
+ co_return; \
+ } while (false)
+
+/**
+ * @brief Coroutine-friendly version of QTEST test macro.
+ **/
+#define QCORO_TEST(actual, testElement) \
+ do { \
+ if (!QTest::qTest(actual, testElement, #actual, #testElement, __FILE__, __LINE__)) \
+ co_return; \
+ } while (false)
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "macros_p.h"
+#include "coroutine.h"
+
+#include <QTimer>
+#include <memory>
+
+namespace QCoro::detail {
+
+//! Base class for co_awaitable waitFor* operations.
+template<typename T>
+class WaitOperationBase {
+public:
+ Q_DISABLE_COPY(WaitOperationBase)
+ QCORO_DEFAULT_MOVE(WaitOperationBase)
+
+ ~WaitOperationBase() = default;
+
+ bool await_resume() noexcept {
+ return !mTimedOut;
+ }
+
+protected:
+ WaitOperationBase(T *obj, int timeout_msecs) : mObj{obj} {
+ if (timeout_msecs > -1) {
+ mTimeoutTimer = std::make_unique<QTimer>();
+ mTimeoutTimer->setInterval(timeout_msecs);
+ mTimeoutTimer->setSingleShot(true);
+ }
+ }
+
+ void startTimeoutTimer(std::coroutine_handle<> awaitingCoroutine) {
+ if (!mTimeoutTimer) {
+ return;
+ }
+
+ QObject::connect(mTimeoutTimer.get(), &QTimer::timeout,
+ [this, awaitingCoroutine]() mutable {
+ mTimedOut = true;
+ resume(awaitingCoroutine);
+ });
+ mTimeoutTimer->start();
+ }
+
+ void resume(std::coroutine_handle<> awaitingCoroutine) {
+ if (mTimeoutTimer) {
+ mTimeoutTimer->stop();
+ }
+
+ QObject::disconnect(mConn);
+
+ QTimer::singleShot(0, [awaitingCoroutine]() mutable { awaitingCoroutine.resume(); });
+ }
+
+ QPointer<T> mObj;
+ std::unique_ptr<QTimer> mTimeoutTimer;
+ QMetaObject::Connection mConn;
+ bool mTimedOut = false;
+};
+
+} // namespace QCoro::detail
--- /dev/null
+# SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+#
+# SPDX-License-Identifier: MIT
+
+add_qcoro_library(
+ NAME WebSockets
+ SOURCES
+ qcorowebsocket.cpp
+ qcorowebsocketserver.cpp
+ CAMELCASE_HEADERS
+ QCoroWebSockets
+ QCoroWebSocket
+ QCoroWebSocketServer
+ QCORO_LINK_LIBRARIES
+ PUBLIC Coro Core
+ QT_LINK_LIBRARIES
+ PUBLIC Core Network WebSockets
+)
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorowebsocket.h"
+#include "qcoroasyncgenerator.h"
+#include "qcorosignal.h"
+
+#include <QWebSocket>
+#include <QDebug>
+
+using namespace QCoro::detail;
+
+using TupleQInt64QByteArray = std::tuple<qint64, QByteArray>;
+using TupleQByteArrayBool = std::tuple<QByteArray, bool>;
+using TupleQStringBool = std::tuple<QString, bool>;
+
+Q_DECLARE_METATYPE(std::optional<TupleQInt64QByteArray>)
+Q_DECLARE_METATYPE(std::optional<TupleQByteArrayBool>)
+Q_DECLARE_METATYPE(std::optional<std::tuple<QByteArray>>)
+Q_DECLARE_METATYPE(std::optional<TupleQStringBool>)
+Q_DECLARE_METATYPE(std::optional<std::tuple<QString>>)
+
+namespace {
+
+class WebSocketStateWatcher : public QObject {
+ Q_OBJECT
+public:
+ WebSocketStateWatcher(QWebSocket *socket, QAbstractSocket::SocketState desiredState)
+ : mState(connect(socket, &QWebSocket::stateChanged, this, [this, desiredState](auto newState) {
+ if (newState == desiredState) {
+ emitReady(true);
+ }
+ }))
+ , mError(connect(socket, qOverload<QAbstractSocket::SocketError>(
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ &QWebSocket::errorOccurred
+#else
+ &QWebSocket::error
+#endif
+ ), this, [this](auto error) {
+ qWarning() << "QWebSocket failed to connect to a websocket server: " << error;
+ emitReady(false);
+ }))
+ {}
+
+Q_SIGNALS:
+ void ready(bool result);
+
+private:
+ void emitReady(bool result) {
+ disconnect(mState);
+ disconnect(mError);
+ Q_EMIT ready(result);
+ }
+
+ QMetaObject::Connection mState;
+ QMetaObject::Connection mError;
+};
+
+template<typename>
+struct signal_args;
+
+template<typename T, typename R, typename ... Args>
+struct signal_args<R(T::*)(Args ...)> {
+ using types = std::tuple<std::remove_cvref_t<Args> ...>;
+};
+
+template<typename T>
+using signal_args_t = typename signal_args<T>::types;
+
+template<typename>
+struct unwrapped_signal_args;
+
+template<typename ... Args>
+struct unwrapped_signal_args<std::tuple<Args ...>> {
+private:
+ using args_tuple = std::tuple<std::remove_cvref_t<Args> ...>;
+public:
+ using type = std::conditional_t<std::tuple_size_v<args_tuple> == 1,
+ std::tuple_element_t<0, args_tuple>,
+ args_tuple>;
+};
+
+template<typename ... Args>
+using unwrapped_signal_args_t = typename unwrapped_signal_args<Args ...>::type;
+
+class WebSocketSignalWatcher : public QObject {
+ Q_OBJECT
+public:
+ template<typename Signal>
+ WebSocketSignalWatcher(QWebSocket *socket, Signal signal) {
+ qRegisterMetaType<std::optional<std::tuple<qint64, QByteArray>>>();
+ qRegisterMetaType<std::optional<std::tuple<QByteArray, bool>>>();
+ qRegisterMetaType<std::optional<std::tuple<QByteArray>>>();
+ qRegisterMetaType<std::optional<std::tuple<QString, bool>>>();
+ qRegisterMetaType<std::optional<std::tuple<QString>>>();
+
+ connect(socket, signal, this, [this](auto && ... args) {
+ Q_EMIT this->ready(std::make_optional(std::make_tuple(std::forward<decltype(args)>(args) ...)));
+ });
+ connect(socket, &QWebSocket::stateChanged, this, [this](auto state) {
+ // In theory, WebSocketSignalWatcher should never be used on
+ // unconnected socket, so maybe the check is redundant
+ if (state != QAbstractSocket::ConnectedState) {
+ Q_EMIT this->ready(std::optional<signal_args_t<Signal>>{});
+ }
+ });
+ }
+
+Q_SIGNALS:
+ void ready(std::optional<std::tuple<qint64, QByteArray>>); // ping
+ void ready(std::optional<std::tuple<QByteArray, bool>>); // binary frames
+ void ready(std::optional<std::tuple<QByteArray>>); // binary messages
+ void ready(std::optional<std::tuple<QString, bool>>); // text frames
+ void ready(std::optional<std::tuple<QString>>); // text messages
+};
+
+template<typename Signal>
+auto watcherGenerator(QWebSocket *ws, Signal signal, std::chrono::milliseconds timeout) ->
+ QCoro::AsyncGenerator<unwrapped_signal_args_t<signal_args_t<Signal>>>
+{
+ WebSocketSignalWatcher watcher(ws, signal);
+ using signalType = std::optional<signal_args_t<Signal>>;
+ auto signalListener = qCoroSignalListener(&watcher, qOverload<signalType>(&WebSocketSignalWatcher::ready), timeout);
+ auto it = co_await signalListener.begin();
+ while (it != signalListener.end()) {
+ if (!(*it).has_value()) {
+ break;
+ }
+ // If the signal is a single-value tuple, we unwrap it from the tuple, otherwise we yield the whole tuple.
+ if constexpr (std::tuple_size_v<typename signalType::value_type> == 1) {
+ co_yield std::get<0>(**it);
+ } else {
+ co_yield **it;
+ }
+ co_await ++it;
+ }
+}
+
+
+} // namespace
+
+QCoroWebSocket::QCoroWebSocket(QWebSocket *socket)
+ : mWebSocket(socket)
+{}
+
+QCoro::Task<bool> QCoroWebSocket::open(const QUrl &url, std::chrono::milliseconds timeout)
+{
+ if (mWebSocket->state() == QAbstractSocket::ConnectedState) {
+ co_return true;
+ }
+
+ WebSocketStateWatcher watcher(mWebSocket, QAbstractSocket::ConnectedState);
+ mWebSocket->open(url);
+ const auto result = co_await qCoro(&watcher, &WebSocketStateWatcher::ready, timeout);
+ co_return result.value_or(false);
+}
+
+QCoro::Task<bool> QCoroWebSocket::open(const QNetworkRequest &request, std::chrono::milliseconds timeout)
+{
+ if (mWebSocket->state() == QAbstractSocket::ConnectedState) {
+ co_return true;
+ }
+
+ WebSocketStateWatcher watcher(mWebSocket, QAbstractSocket::ConnectedState);
+ mWebSocket->open(request);
+ const auto result = co_await qCoro(&watcher, &WebSocketStateWatcher::ready, timeout);
+ co_return result.value_or(false);
+}
+
+QCoro::Task<std::optional<std::chrono::milliseconds>> QCoroWebSocket::ping(const QByteArray &payload,
+ std::chrono::milliseconds timeout)
+{
+ if (mWebSocket->state() != QAbstractSocket::ConnectedState) {
+ co_return std::nullopt;
+ }
+
+ WebSocketSignalWatcher watcher(mWebSocket, &QWebSocket::pong);
+ mWebSocket->ping(payload);
+ const auto result = co_await qCoro(&watcher, qOverload<std::optional<std::tuple<qint64, QByteArray>>>(&WebSocketSignalWatcher::ready), timeout);
+ if (result.has_value() && (*result).has_value()) {
+ co_return std::chrono::milliseconds{std::get<0>(**result)};
+ }
+ co_return std::nullopt;
+}
+
+QCoro::AsyncGenerator<std::tuple<QByteArray, bool>> QCoroWebSocket::binaryFrames(
+ std::chrono::milliseconds timeout)
+{
+ return watcherGenerator(mWebSocket, &QWebSocket::binaryFrameReceived, timeout);
+}
+
+QCoro::AsyncGenerator<QByteArray> QCoroWebSocket::binaryMessages(
+ std::chrono::milliseconds timeout)
+{
+ return watcherGenerator(mWebSocket, &QWebSocket::binaryMessageReceived, timeout);
+}
+
+QCoro::AsyncGenerator<std::tuple<QString, bool>> QCoroWebSocket::textFrames(
+ std::chrono::milliseconds timeout)
+{
+ return watcherGenerator(mWebSocket, &QWebSocket::textFrameReceived, timeout);
+}
+
+QCoro::AsyncGenerator<QString> QCoroWebSocket::textMessages(
+ std::chrono::milliseconds timeout)
+{
+ return watcherGenerator(mWebSocket, &QWebSocket::textMessageReceived, timeout);
+}
+
+#include "qcorowebsocket.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "qcoroasyncgenerator.h"
+#include "qcorowebsockets_export.h"
+
+#include <QWebSocketProtocol>
+
+#include <tuple>
+#include <chrono>
+#include <optional>
+
+class QWebSocket;
+class QNetworkRequest;
+class QUrl;
+
+namespace QCoro::detail {
+
+class QCOROWEBSOCKETS_EXPORT QCoroWebSocket {
+public:
+ explicit QCoroWebSocket(QWebSocket *websocket);
+
+ Task<bool> open(const QUrl &url, std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+ Task<bool> open(const QNetworkRequest &request, std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ Task<std::optional<std::chrono::milliseconds>> ping(const QByteArray &payload, std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ AsyncGenerator<std::tuple<QByteArray, bool>> binaryFrames(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+ AsyncGenerator<QByteArray> binaryMessages(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+ AsyncGenerator<std::tuple<QString, bool>> textFrames(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+ AsyncGenerator<QString> textMessages(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+private:
+ QWebSocket *mWebSocket;
+};
+
+} // namespace QCoro::detail
+
+auto inline qCoro(QWebSocket *websocket) noexcept {
+ return QCoro::detail::QCoroWebSocket{websocket};
+}
+
+auto inline qCoro(QWebSocket &websocket) noexcept {
+ return QCoro::detail::QCoroWebSocket{&websocket};
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorowebsocket.h"
+#include "qcorowebsocketserver.h"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorowebsocketserver.h"
+#include "qcorosignal.h"
+
+#include <QWebSocket>
+#include <QWebSocketServer>
+
+using namespace QCoro::detail;
+
+namespace {
+
+class QCoroWebSocketServerSignalListener : public QObject {
+ Q_OBJECT
+public:
+ QCoroWebSocketServerSignalListener(QWebSocketServer *server) {
+ connect(server, &QWebSocketServer::closed, this, [this]() {
+ Q_EMIT ready(nullptr);
+ });
+ connect(server, &QWebSocketServer::newConnection, [this, server]() {
+ Q_EMIT ready(server->nextPendingConnection());
+ });
+ }
+
+Q_SIGNALS:
+ void ready(QWebSocket *socket);
+};
+
+} // namespace
+
+QCoroWebSocketServer::QCoroWebSocketServer(QWebSocketServer *server)
+ : mServer(server)
+{}
+
+QCoro::Task<QWebSocket *> QCoroWebSocketServer::nextPendingConnection(std::chrono::milliseconds timeout)
+{
+ auto * const server = mServer;
+ if (!server->isListening()) {
+ co_return nullptr;
+ }
+
+ if (server->hasPendingConnections()) {
+ co_return server->nextPendingConnection();
+ }
+
+ QCoroWebSocketServerSignalListener listener(server);
+ const auto result = co_await qCoro(&listener, &QCoroWebSocketServerSignalListener::ready, timeout);
+ co_return result.value_or(nullptr);
+}
+
+#include "qcorowebsocketserver.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "qcorotask.h"
+#include "qcorowebsockets_export.h"
+
+#include <chrono>
+
+class QWebSocket;
+class QWebSocketServer;
+
+namespace QCoro::detail {
+
+class QCOROWEBSOCKETS_EXPORT QCoroWebSocketServer {
+public:
+ explicit QCoroWebSocketServer(QWebSocketServer *server);
+
+ Task<QWebSocket *> nextPendingConnection(std::chrono::milliseconds timeout = std::chrono::milliseconds{-1});
+
+private:
+ QWebSocketServer *mServer;
+};
+
+
+
+} // namespace QCoro::detail
+
+auto inline qCoro(QWebSocketServer *server) noexcept {
+ return QCoro::detail::QCoroWebSocketServer{server};
+}
+
+auto inline qCoro(QWebSocketServer &server) noexcept {
+ return QCoro::detail::QCoroWebSocketServer{&server};
+}
--- /dev/null
+setuptools
+wheel
+pymdown-extensions~=10.11
+pygments~=2.19
+mkdocs
+mkdocs-material
+mkdocs-section-index
+mkdocs-include-markdown-plugin
+mkdocs-macros-plugin
+mkdocs-blogging-plugin
+mkdocs-rss-plugin
\ No newline at end of file
--- /dev/null
+add_subdirectory(testlibs)
+
+include(CMakeParseArguments)
+
+function(_enable_supressions _name)
+ function(_enable_supressions_file _filename)
+ if (EXISTS "${_filename}")
+ set_tests_properties(test-${_name} PROPERTIES
+ ENVIRONMENT LSAN_OPTIONS=suppressions=${_filename}
+ )
+ endif()
+ endfunction()
+
+ _enable_supressions_file("${CMAKE_CURRENT_SOURCE_DIR}/${_name}-qt${QT_VERSION_MAJOR}-lsan.supp")
+endfunction()
+
+function(qcoro_add_test _name)
+ set(options SKIP_ADD_TEST)
+ set(oneValueArgs)
+ set(multiValueAgs LINK_LIBRARIES)
+ cmake_parse_arguments(TEST "${options}" "${oneValueArgs}" "${multiValueAgs}" ${ARGN})
+ add_executable(test-${_name} ${_name}.cpp)
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ qcoro_testlib
+ QCoro${QT_VERSION_MAJOR}Core
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Test
+ ${TEST_LINK_LIBRARIES}
+ Threads::Threads
+ )
+ set_target_defaults(test-${_name})
+ if (NOT TEST_SKIP_ADD_TEST)
+ add_test(NAME test-${_name} COMMAND test-${_name})
+ _enable_supressions(${_name})
+ endif()
+
+ target_code_coverage(test-${_name} AUTO ALL EXCLUDE "${CMAKE_BINARY_DIR}" "tests/testlibs/*")
+endfunction()
+
+function(qcoro_add_network_test _name)
+ qcoro_add_test(${_name} ${ARGN})
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}Network
+ Qt${QT_VERSION_MAJOR}::Network
+ )
+ if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB)
+ target_link_libraries(test-${_name} PRIVATE atomic)
+ endif()
+endfunction()
+
+function(qcoro_add_dbus_test _name)
+ qcoro_add_test(${_name} ${ARGN} SKIP_ADD_TEST)
+ add_dependencies(test-${_name} testdbusserver qcoro_test_dbus)
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}DBus
+ qcoro_test_dbus
+ )
+ target_compile_definitions(test-${_name} PRIVATE TESTDBUSSERVER_EXECUTABLE=\"$<TARGET_FILE:testdbusserver>\")
+ if (APPLE)
+ # On MacOS dbus-launch doesn't work, so we rely on the session dbus running
+ add_test(NAME test-${_name} COMMAND test-${_name})
+ else()
+ add_test(NAME test-${_name} COMMAND dbus-launch $<TARGET_FILE:test-${_name}>)
+ endif()
+ _enable_supressions(${_name})
+endfunction()
+
+function(qcoro_add_websockets_test _name)
+ qcoro_add_network_test(${_name} ${ARGN})
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}WebSockets
+ Qt${QT_VERSION_MAJOR}::WebSockets
+ qcoro_test_ws
+ )
+endfunction()
+
+function(qcoro_add_qml_test _name)
+ qcoro_add_network_test(${_name} ${ARGN})
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}Qml
+ Qt${QT_VERSION_MAJOR}::Qml
+ )
+endfunction()
+
+function(qcoro_add_quick_test _name)
+ qcoro_add_test(${_name} ${ARGN})
+ target_link_libraries(
+ test-${_name}
+ PRIVATE
+ QCoro${QT_VERSION_MAJOR}Quick
+ Qt${QT_VERSION_MAJOR}::Quick
+ Qt${QT_VERSION_MAJOR}::QuickPrivate
+ )
+ get_test_property(test-${_name} ENVIRONMENT _env)
+ list(APPEND _env QT_QPA_PLATFORM=offscreen)
+ set_tests_properties(test-${_name} PROPERTIES ENVIRONMENT "${_env}")
+endfunction()
+
+qcoro_add_test(qtimer)
+qcoro_add_test(qcoroprocess)
+qcoro_add_test(qcorosignal)
+qcoro_add_test(qcorothread)
+qcoro_add_test(qcorotask)
+qcoro_add_test(qcorolazytask)
+qcoro_add_test(testconstraints)
+qcoro_add_test(qfuture LINK_LIBRARIES Qt${QT_VERSION_MAJOR}::Concurrent)
+qcoro_add_test(qcorogenerator)
+qcoro_add_test(qcoroasyncgenerator)
+qcoro_add_test(qcorowaitfor)
+
+if (QCORO_WITH_QTDBUS)
+ qcoro_add_dbus_test(qdbuspendingcall)
+ qcoro_add_dbus_test(qdbuspendingreply)
+endif()
+
+if (QCORO_WITH_QTNETWORK)
+ qcoro_add_network_test(qcorolocalsocket)
+ qcoro_add_network_test(qcoroabstractsocket)
+ qcoro_add_network_test(qcoronetworkreply)
+ qcoro_add_network_test(qcorotcpserver)
+
+ # Tests for test utilities
+ qcoro_add_network_test(testhttpserver)
+endif()
+
+if (QCORO_WITH_QTWEBSOCKETS)
+ qcoro_add_websockets_test(qcorowebsocket)
+ qcoro_add_websockets_test(qcorowebsocketserver)
+endif()
+
+if (QCORO_WITH_QML)
+ qcoro_add_qml_test(qcoroqmltask)
+endif()
+
+if (QCORO_WITH_QTQUICK)
+ qcoro_add_quick_test(qcoroimageprovider)
+endif()
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testhttpserver.h"
+#include "testobject.h"
+#include "qcoroiodevice_macros.h"
+
+#include "qcoro/network/qcoroabstractsocket.h"
+
+#include <QTcpServer>
+#include <QTcpSocket>
+
+#include <thread>
+
+class QCoroAbstractSocketTest : public QCoro::TestObject<QCoroAbstractSocketTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testWaitForConnectedTriggers_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+ QCORO_DELAY(socket.connectToHost(QHostAddress::LocalHost, mServer.port()));
+
+ co_await qCoro(socket).waitForConnected();
+
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ // Make sure the server gets the connection as well
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForConnectedTriggers_coro(TestLoop &el) {
+ QTcpSocket socket;
+ QCORO_DELAY(socket.connectToHost(QHostAddress::LocalHost, mServer.port()));
+ bool called = false;
+ qCoro(socket).waitForConnected().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testWaitForDisconnectedTriggers_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ QCORO_DELAY(socket.disconnectFromHost());
+
+ co_await qCoro(socket).waitForDisconnected();
+
+ QCORO_COMPARE(socket.state(), QAbstractSocket::UnconnectedState);
+
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForDisconnectedTriggers_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ QCORO_DELAY(socket.disconnectFromHost());
+
+ qCoro(socket).waitForDisconnected().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ });
+ el.exec();
+
+ QVERIFY(called);
+
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testDoesntCoAwaitConnectedSocket_coro(QCoro::TestContext context) {
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ context.setShouldNotSuspend();
+ co_await qCoro(socket).waitForConnected();
+
+ socket.write("GET / HTTP/1.1\r\n");
+
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenDoesntCoAwaitConnectedSocket_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ qCoro(socket).waitForConnected().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ });
+ el.exec();
+
+ QVERIFY(called);
+
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testDoesntCoAwaitDisconnectedSocket_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+ mServer.setExpectTimeout(true); // no-one actually connects, so the server times out.
+
+ QTcpSocket socket;
+ QCORO_COMPARE(socket.state(), QAbstractSocket::UnconnectedState);
+
+ co_await qCoro(socket).waitForDisconnected();
+ }
+
+ void testThenDoesntCoAwaitDisconnectedSocket_coro(TestLoop &el) {
+ mServer.setExpectTimeout(true); // no-one actually connects, so the server time out.
+
+ QTcpSocket socket;
+ QCOMPARE(socket.state(), QAbstractSocket::UnconnectedState);
+
+ bool called = false;
+ qCoro(socket).waitForDisconnected().then([&](bool disconnected) {
+ called = true;
+ el.quit();
+ QVERIFY(!disconnected);
+ });
+ el.exec();
+
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testConnectToServerWithArgs_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenConnectToServerWithArgs_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testWaitForConnectedTimeout_coro(QCoro::TestContext) {
+ mServer.setExpectTimeout(true);
+ QTcpSocket socket;
+
+ QCORO_TEST_TIMEOUT(co_await qCoro(socket).waitForConnected(10ms));
+ }
+
+ void testThenWaitForConnectedTimeout_coro(TestLoop &el) {
+ mServer.setExpectTimeout(true);
+
+ QTcpSocket socket;
+ const auto start = std::chrono::steady_clock::now();
+ bool called = false;
+ qCoro(socket).waitForConnected(10ms).then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(!connected);
+ });
+ el.exec();
+ const auto end = std::chrono::steady_clock::now();
+ QVERIFY(end - start < 500ms);
+
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testWaitForDisconnectedTimeout_coro(QCoro::TestContext) {
+ mServer.setExpectTimeout(true);
+
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ QCORO_TEST_TIMEOUT(co_await qCoro(socket).waitForDisconnected(10ms));
+
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForDisconnectedTimeout_coro(TestLoop &el) {
+ mServer.setExpectTimeout(true);
+
+ QTcpSocket socket;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ const auto start = std::chrono::steady_clock::now();
+ qCoro(socket).waitForDisconnected(10ms).then([&el, start](bool disconnected) {
+ el.quit();
+ QVERIFY(!disconnected);
+ const auto end = std::chrono::steady_clock::now();
+ QVERIFY(end - start < 500ms);
+ });
+ });
+ el.exec();
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadAllTriggers_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /stream HTTP/1.1\r\n");
+
+ QCORO_TEST_IODEVICE_READALL(socket);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadAllTriggers_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /block HTTP/1.1\r\n");
+
+ QByteArray data;
+ qCoro(socket).readAll().then([&](const QByteArray &data) {
+ el.quit();
+ called = true;
+ QVERIFY(!data.isEmpty());
+ });
+ });
+ el.exec();
+
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadTriggers_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /stream HTTP/1.1\r\n");
+
+ QCORO_TEST_IODEVICE_READ(socket);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadTriggers_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /block HTTP/1.1\r\n");
+
+ QByteArray data;
+ qCoro(socket).read(1).then([&](const QByteArray &data) {
+ el.quit();
+ called = true;
+ QCOMPARE(data.size(), 1);
+ });
+ });
+ el.exec();
+
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadLineTriggers_coro(QCoro::TestContext) {
+ QTcpSocket socket;
+ co_await qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port());
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /stream HTTP/1.1\r\n");
+
+ QCORO_TEST_IODEVICE_READLINE(socket);
+ QCORO_COMPARE(lines.size(), 14);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadLineTriggers_coro(TestLoop &el) {
+ QTcpSocket socket;
+ bool called = false;
+ qCoro(socket).connectToHost(QHostAddress::LocalHost, mServer.port()).then([&](bool connected) {
+ if (!connected) {
+ el.quit();
+ }
+ QVERIFY(connected);
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ socket.write("GET /stream HTTP/1.1\r\n");
+
+ QByteArray data;
+ qCoro(socket).readLine().then([&](const QByteArray &data) {
+ el.quit();
+ called = true;
+ QVERIFY(!data.isEmpty());
+ });
+ });
+ el.exec();
+
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+private Q_SLOTS:
+ void init() {
+ mServer.start(QHostAddress::LocalHost);
+ }
+
+ void cleanup() {
+ mServer.stop();
+ }
+
+ addCoroAndThenTests(WaitForConnectedTriggers)
+ addCoroAndThenTests(WaitForConnectedTimeout)
+ addCoroAndThenTests(WaitForDisconnectedTriggers)
+ addCoroAndThenTests(WaitForDisconnectedTimeout)
+ addCoroAndThenTests(DoesntCoAwaitConnectedSocket)
+ addCoroAndThenTests(DoesntCoAwaitDisconnectedSocket)
+ addCoroAndThenTests(ConnectToServerWithArgs)
+ addCoroAndThenTests(ReadAllTriggers)
+ addCoroAndThenTests(ReadTriggers)
+ addCoroAndThenTests(ReadLineTriggers)
+
+private:
+ TestHttpServer<QTcpServer> mServer;
+};
+
+QTEST_GUILESS_MAIN(QCoroAbstractSocketTest)
+
+#include "qcoroabstractsocket.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroasyncgenerator.h"
+#include "qcorotimer.h"
+#include "testobject.h"
+
+#include <cstdint>
+#include <vector>
+
+#include <QScopeGuard>
+
+struct Nocopymove {
+ explicit constexpr Nocopymove(int val): val(val) {}
+ Nocopymove(const Nocopymove &) = delete;
+ Nocopymove &operator=(const Nocopymove &) = delete;
+ Nocopymove(Nocopymove &&) = delete;
+ Nocopymove &operator=(Nocopymove &&) = delete;
+ ~Nocopymove() = default;
+
+ int val;
+};
+
+struct Moveonly {
+ explicit constexpr Moveonly(int val): val(val) {}
+ Moveonly(const Moveonly &) = delete;
+ Moveonly &operator=(const Moveonly &) = delete;
+ Moveonly(Moveonly &&) noexcept = default;
+ Moveonly &operator=(Moveonly &&) noexcept = default;
+ ~Moveonly() = default;
+
+ int val;
+};
+
+QCoro::Task<> sleep(std::chrono::milliseconds ms) {
+ QTimer timer;
+ timer.start(ms);
+ co_await timer;
+}
+
+class AsyncGeneratorTest : public QCoro::TestObject<AsyncGeneratorTest> {
+ Q_OBJECT
+private:
+ QCoro::Task<> testGenerator_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ for (int i = 0; i < 10; i++) {
+ QTimer timer;
+ timer.start(50ms);
+ co_await qCoro(timer).waitForTimeout();
+ co_yield i;
+ }
+ };
+
+ std::vector<int> values;
+ QCORO_FOREACH(int val, createGenerator()) {
+ values.push_back(val);
+ }
+
+ QCORO_COMPARE(values, (std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
+ }
+
+ QCoro::Task<> testSyncGenerator_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ for (int i = 0; i < 10; ++i) {
+ co_yield i;
+ }
+ };
+
+ std::vector<int> values;
+ QCORO_FOREACH(int val, createGenerator()) {
+ values.push_back(val);
+ }
+
+ QCORO_COMPARE(values, (std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
+ }
+
+ QCoro::Task<> testTerminateSuspendedGenerator_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ bool destroyed = false;
+ const auto createGenerator = [&destroyed]() -> QCoro::AsyncGenerator<int> {
+ const auto guard = qScopeGuard([&destroyed]() {
+ destroyed = true;
+ });
+
+ const auto pointer = std::make_unique<QString>(
+ QStringLiteral("This should get destroyed. If not, ASAN will catch it."));
+
+ while (true) {
+ co_yield 42;
+ }
+ };
+
+ {
+ auto generator = createGenerator();
+ const auto it = co_await generator.begin();
+ QCORO_COMPARE(*it, 42);
+ } // The generator gets destroyed here. Everything on generator's stack is destroyed.
+
+ QCORO_VERIFY(destroyed);
+ }
+
+ QCoro::Task<> testEmptyGenerator_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ if (false) { // NOLINT(readability-simplify-boolean-expr)
+ co_yield 42;
+ }
+ };
+
+ auto generator = createGenerator();
+ QCORO_COMPARE(co_await generator.begin(), generator.end());
+ }
+
+ QCoro::Task<> testReferenceGenerator_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<Nocopymove &> {
+ for (int i = 0; i < 8; i += 2) {
+ Nocopymove val{i};
+ co_await sleep(10ms);
+ co_yield val;
+ QCORO_COMPARE(val.val, i + 1);
+ }
+ };
+
+ int testvalue = 0;
+ QCORO_FOREACH(Nocopymove &val, createGenerator()) {
+ QCORO_COMPARE(val.val, testvalue);
+ ++val.val;
+ testvalue += 2;
+ }
+ QCORO_COMPARE(testvalue, 8);
+ }
+
+ QCoro::Task<> testConstReferenceGenerator_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<const Nocopymove &> {
+ for (int i = 0; i < 4; ++i) {
+ const Nocopymove value{i};
+ co_await sleep(10ms);
+ co_yield value;
+ }
+ };
+
+ int testvalue = 0;
+ QCORO_FOREACH(const Nocopymove &val, createGenerator()) {
+ QCORO_COMPARE(val.val, testvalue++);
+ }
+ QCORO_COMPARE(testvalue, 4);
+ }
+
+ QCoro::Task<> testMoveonlyGenerator_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<Moveonly> {
+ for (int i = 0; i < 4; ++i) {
+ Moveonly value{i};
+ co_await sleep(10ms);
+ co_yield std::move(value);
+ }
+ };
+
+ auto generator = createGenerator();
+ int testvalue = 0;
+ for (auto it = co_await generator.begin(), end = generator.end(); it != end; co_await ++it) {
+ Moveonly value = std::move(*it);
+ QCORO_COMPARE(value.val, testvalue++);
+ }
+ QCORO_COMPARE(testvalue, 4);
+ }
+
+ QCoro::Task<> testMovedGenerator_coro(QCoro::TestContext) {
+ QCoro::AsyncGenerator<int> generator;
+
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ for (int i = 0; i < 4; ++i) {
+ co_await sleep(10ms);
+ co_yield i;
+ }
+ };
+
+ {
+ QCoro::AsyncGenerator<int> originalGenerator = createGenerator();
+ generator = std::move(originalGenerator);
+ }
+ int testvalue = 0;
+ for (auto it = co_await generator.begin(), end = generator.end(); it != end; co_await ++it) {
+ int value = *it;
+ QCORO_COMPARE(value, testvalue++);
+ }
+ QCORO_COMPARE(testvalue, 4);
+ }
+
+ QCoro::Task<> testException_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ for (int i = 0; i < 4; ++i) {
+ co_await sleep(10ms);
+ if (i == 2) {
+ throw std::runtime_error("Two?! I can't handle that much!");
+ }
+ co_yield i;
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = co_await generator.begin();
+ QCORO_VERIFY(it != generator.end());
+ QCORO_COMPARE(*it, 0);
+ co_await ++it;
+ QCORO_VERIFY(it != generator.end());
+ QCORO_COMPARE(*it, 1);
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await ++it, std::runtime_error);
+ QCORO_COMPARE(it, generator.end());
+ }
+
+ QCoro::Task<> testExceptionInDereference_coro(QCoro::TestContext) {
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ for (int i = 0; i < 4; ++i) {
+ co_await sleep(10ms);
+ if (i == 2) {
+ throw std::runtime_error("I already told you two is too much");
+ }
+ co_yield i;
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = co_await generator.begin();
+ QCORO_VERIFY(it != generator.end());
+ QCORO_COMPARE(*it, 0);
+ co_await ++it;
+ QCORO_VERIFY(it != generator.end());
+ QCORO_COMPARE(*it, 1);
+ co_await ++it;
+ QCORO_VERIFY_EXCEPTION_THROWN(*it, std::runtime_error);
+ QCORO_COMPARE(it, generator.end());
+ }
+
+ QCoro::Task<> testExceptionInBegin_coro(QCoro::TestContext) {
+ bool throw_exception = true;
+ const auto createGenerator = [throw_exception]() -> QCoro::AsyncGenerator<uint64_t> {
+ co_await sleep(10ms);
+ // NOTE: The condition here is a necessary workaround for Clang being too clever,
+ // seeing that `co_yield` will never be reached and optimizing it away, thus breaking
+ // the coroutine. With this condition (or by wrapping the body into a for-loop) the
+ // optimization is disabled (as the co_yield could *theoretically* be reached) and
+ // the generator behaves as expected.
+ if (throw_exception) {
+ throw std::runtime_error("I can't even zero!");
+ }
+ co_yield 42;
+ };
+
+ auto generator = createGenerator();
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await generator.begin(), std::runtime_error);
+ }
+
+ QCoro::Task<> testExceptionInBeginSync_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ const auto createGenerator = []() -> QCoro::AsyncGenerator<int> {
+ throw std::runtime_error("I can't even zero!");
+ co_yield 1;
+ };
+
+ auto generator = createGenerator();
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await generator.begin(), std::runtime_error);
+ }
+
+
+private Q_SLOTS:
+ addTest(Generator)
+ addTest(SyncGenerator)
+ addTest(TerminateSuspendedGenerator)
+ addTest(EmptyGenerator)
+ addTest(ReferenceGenerator)
+ addTest(ConstReferenceGenerator)
+ addTest(MoveonlyGenerator)
+ addTest(MovedGenerator)
+ addTest(Exception)
+ addTest(ExceptionInDereference)
+ addTest(ExceptionInBegin)
+ addTest(ExceptionInBeginSync)
+};
+
+QTEST_GUILESS_MAIN(AsyncGeneratorTest)
+
+#include "qcoroasyncgenerator.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorogenerator.h"
+#include "qcorotest.h"
+
+#include <QObject>
+#include <QTest>
+#include <QScopeGuard>
+
+struct Nocopymove {
+ explicit constexpr Nocopymove(int val): val(val) {}
+ Nocopymove(const Nocopymove &) = delete;
+ Nocopymove &operator=(const Nocopymove &) = delete;
+ Nocopymove(Nocopymove &&) = delete;
+ Nocopymove &operator=(Nocopymove &&) = delete;
+ ~Nocopymove() = default;
+
+ int val;
+};
+
+struct Moveonly {
+ explicit constexpr Moveonly(int val): val(val) {}
+ Moveonly(const Moveonly &) = delete;
+ Moveonly &operator=(const Moveonly &) = delete;
+ Moveonly(Moveonly &&) noexcept = default;
+ Moveonly &operator=(Moveonly &&) noexcept = default;
+ ~Moveonly() = default;
+
+ int val;
+};
+
+class GeneratorTest : public QObject {
+ Q_OBJECT
+private Q_SLOTS:
+
+ void testImmediateGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<int> {
+ for (int value = 0; value < 10; ++value) {
+ co_yield value;
+ }
+ };
+
+ auto generator = createGenerator();
+ std::vector<int> values;
+ for (auto it = generator.begin(), end = generator.end(); it != end; ++it) {
+ values.emplace_back(*it);
+ }
+
+ QCOMPARE(values.size(), 10U);
+ QCOMPARE(values, (std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
+ }
+
+ void testTerminateSuspendedGenerator() {
+ bool destroyed = false;
+ const auto createGenerator = [&destroyed]() -> QCoro::Generator<int> {
+ const auto guard = qScopeGuard([&destroyed]() {
+ destroyed = true;
+ });
+
+ const auto pointer = std::make_unique<QString>(
+ QStringLiteral("This should get destroyed. If not, ASAN will catch it."));
+
+ while (true) {
+ co_yield 42;
+ }
+ };
+
+ {
+ auto generator = createGenerator();
+ const auto it = generator.begin();
+ QCOMPARE(*it, 42);
+ } // The generator gets destroyed here.
+
+ QVERIFY(destroyed);
+ }
+
+ void testEmptyGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<int> {
+ if (false) { // NOLINT(readability-simplify-boolean-expr)
+ co_yield 42; // Make it a coroutine, except it never gets invoked.
+ }
+ };
+
+ auto generator = createGenerator();
+ const auto begin = generator.begin();
+ QCOMPARE(begin, generator.end());
+ }
+
+ void testConstReferenceGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<const Nocopymove &> {
+ for (int i = 0; i < 4; ++i) {
+ const Nocopymove val(i);
+ co_yield val;
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = generator.begin();
+ int testval = 0;
+ while (it != generator.end()) {
+ const Nocopymove &value = *it;
+ QCOMPARE(value.val, testval++);
+ ++it;
+ }
+ QCOMPARE(testval, 4);
+ }
+
+ void testReferenceGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<Nocopymove &> {
+ for (int i = 0; i < 8; i += 2) {
+ Nocopymove val(i);
+ co_yield val;
+ QCORO_COMPARE(val.val, i + 1);
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = generator.begin();
+ int testval = 0;
+ while (it != generator.end()) {
+ Nocopymove &value = *it;
+ QCOMPARE(value.val, testval);
+ value.val += 1;
+ testval += 2;
+ ++it;
+ }
+ }
+
+ void testMoveonlyGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<Moveonly &&> {
+ for (int i = 0; i < 4; ++i) {
+ Moveonly value{i};
+ co_yield std::move(value);
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = generator.begin();
+ int testval = 0;
+ while (it != generator.end()) {
+ Moveonly val = std::move(*it);
+ QCOMPARE(val.val, testval++);
+ ++it;
+ }
+ QCOMPARE(testval, 4);
+ }
+
+ void testMovedGenerator() {
+ const auto createGenerator = []() -> QCoro::Generator<int> {
+ for (int i = 0; i < 4; ++i) {
+ co_yield i;
+ }
+ };
+
+ auto originalGenerator = createGenerator();
+ auto generator = std::move(originalGenerator);
+ auto it = generator.begin();
+ int testval = 0;
+ while (it != generator.end()) {
+ int value = *it;
+ QCOMPARE(value, testval++);
+ ++it;
+ }
+ QCOMPARE(testval, 4);
+ }
+
+ void testException() {
+ const auto createGenerator = []() -> QCoro::Generator<int> {
+ for (int i = 0; i < 10; ++i) {
+ if (i == 2) {
+ throw std::runtime_error("Two?! I can't handle two!!");
+ }
+ co_yield i;
+ }
+ };
+
+ auto generator = createGenerator();
+ auto it = generator.begin();
+ QVERIFY(it != generator.end());
+ QCOMPARE(*it, 0);
+ ++it;
+ QVERIFY(it != generator.end());
+ QCOMPARE(*it, 1);
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
+ QVERIFY_THROWS_EXCEPTION(std::runtime_error, ++it);
+#else
+ QVERIFY_EXCEPTION_THROWN(++it, std::runtime_error);
+#endif
+ QCOMPARE(it, generator.end());
+ }
+
+ void testExceptionInBegin() {
+ auto generator = []() -> QCoro::Generator<int> {
+ throw std::runtime_error("Zero is too small!");
+ co_yield 1;
+ }();
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
+ QVERIFY_THROWS_EXCEPTION(std::runtime_error, generator.begin());
+#else
+ QVERIFY_EXCEPTION_THROWN(generator.begin(), std::runtime_error);
+#endif
+ }
+};
+
+QTEST_GUILESS_MAIN(GeneratorTest)
+
+#include "qcorogenerator.moc"
--- /dev/null
+# leaks in Qt 5.15.2 in the Offscreen QPA, fixed in newer Qt 5.15
+leak:QPlatformScreen
+leak:libqoffscreen.so
+
+# leaks in Qt 5.15.2 + GCC 10, 11
+leak:QObject::QObject(QObject*)
+leak:QtSharedPointer::ExternalRefCountData::getAndRef(QObject const*)
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+#include "qcoro/core/qcorotimer.h"
+#include "qcoro/quick/qcoroimageprovider.h"
+
+#include <QQmlEngine>
+#include <QQmlComponent>
+#include <QQuickItem>
+
+#define emit Q_EMIT // HACK: the private header uses `emit`, but QCoro is built with QT_NO_KEYWORDS
+#include <private/qquickimage_p.h>
+#undef emit
+
+class TestImageProvider final: public QCoro::ImageProvider {
+public:
+ TestImageProvider(bool async, const QString &error)
+ : mAsync(async), mError(error)
+ {}
+
+protected:
+ QCoro::Task<QImage> asyncRequestImage(const QString &id, const QSize &requestedSize) override {
+ Q_UNUSED(id);
+ Q_UNUSED(requestedSize);
+
+ if (mAsync) {
+ QTimer timer;
+ timer.start(250ms);
+ co_await timer;
+ }
+
+ co_return QImage{};
+ }
+
+private:
+ bool mAsync;
+ QString mError;
+};
+
+class QCoroImageProviderTest: public QCoro::TestObject<QCoroImageProviderTest> {
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testImageProvider_data() {
+ QTest::addColumn<QString>("id");
+ QTest::addColumn<bool>("async");
+ QTest::addColumn<QString>("error");
+
+ QTest::newRow("sync") << QStringLiteral("image-sync.jpg") << false << QString();
+ QTest::newRow("async") << QStringLiteral("image-async.jpg") << true << QString();
+ }
+
+ void testImageProvider() {
+ QFETCH(QString, id);
+ QFETCH(bool, async);
+ QFETCH(QString, error);
+
+ QString source = QStringLiteral("image://qcorotest/%1.jpg").arg(id);
+
+ auto provider = new TestImageProvider(async, error);
+
+ QQmlEngine engine;
+ engine.addImageProvider(QStringLiteral("qcorotest"), provider);
+ QVERIFY(engine.imageProvider(QStringLiteral("qcorotest")) != nullptr);
+
+ const QString componentStr = QStringLiteral(R"(
+import QtQuick 2.0
+
+Image {
+ source: "%1";
+ asynchronous: %2;
+})").arg(source, async ? QStringLiteral("true") : QStringLiteral("false"));
+
+ QQmlComponent component(&engine);
+ component.setData(componentStr.toLatin1(), QUrl::fromLocalFile(QStringLiteral("")));
+
+ std::unique_ptr<QQuickImage> image{qobject_cast<QQuickImage *>(component.create())};
+ QVERIFY(image != nullptr);
+
+ if (async) {
+ QTRY_COMPARE(image->status(), QQuickImage::Loading);
+ }
+
+ QCOMPARE(image->source(), QUrl(source));
+
+ if (error.isEmpty()) {
+ QTRY_COMPARE(image->status(), QQuickImage::Ready);
+
+ QCOMPARE(image->progress(), 1.0);
+ } else {
+ QTRY_COMPARE(image->status(), QQuickImage::Error);
+ }
+ }
+
+private:
+};
+
+QTEST_MAIN(QCoroImageProviderTest)
+
+#include "qcoroimageprovider.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#define QCORO_TEST_IODEVICE_READALL(device) \
+ QByteArray data; \
+ Q_FOREVER { \
+ const auto buf = co_await qCoro((device)).readAll(); \
+ if (buf.isNull()) { \
+ break; \
+ } \
+ data += buf; \
+ } \
+ QCORO_VERIFY(!data.isEmpty()); \
+ QCORO_COMPARE((device).bytesAvailable(), 0)
+
+#define QCORO_TEST_IODEVICE_READ(device) \
+ QByteArray data; \
+ Q_FOREVER { \
+ const auto buf = co_await qCoro((device)).read(1); \
+ if (buf.isNull()) { \
+ break; \
+ } \
+ data += buf; \
+ } \
+ QCORO_VERIFY(!data.isEmpty()); \
+ QCORO_COMPARE((device).bytesAvailable(), 0)
+
+#define QCORO_TEST_IODEVICE_READLINE(device) \
+ QByteArrayList lines; \
+ Q_FOREVER { \
+ const auto buf = co_await qCoro((device)).readLine(); \
+ if (buf.isNull()) { \
+ break; \
+ } \
+ lines.push_back(buf); \
+ } \
+ QCORO_COMPARE((device).bytesAvailable(), 0)
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorotest.h"
+#include "testobject.h"
+#include "qcorotimer.h"
+#include <qobject.h>
+#include "qcorolazytask.h"
+
+using namespace std::chrono_literals;
+
+class QCoroLazyTaskTest : public QCoro::TestObject<QCoroLazyTaskTest>
+{
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testSyncLazyCoroutineStarts_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ constexpr auto coro = [](bool &started) -> QCoro::LazyTask<> {
+ started = true;
+ co_return;
+ };
+
+ bool started = false;
+ const auto task = coro(started);
+ QCORO_VERIFY(!started);
+
+ co_await task;
+
+ QCORO_VERIFY(started);
+ }
+
+ QCoro::Task<> testLazyCoroutineStarts_coro(QCoro::TestContext) {
+ constexpr auto coro = [](bool &started, bool &resumed) -> QCoro::LazyTask<> {
+ started = true;
+ co_await QCoro::sleepFor(1ms);
+ resumed = true;
+ };
+
+ bool started = false;
+ bool resumed = false;
+ const auto task = coro(started, resumed);
+ QCORO_VERIFY(!started);
+
+ co_await task;
+
+ QCORO_VERIFY(started);
+ QCORO_VERIFY(resumed);
+ }
+
+ QCoro::Task<> testNonVoidSyncLazyCoroutineStarts_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ constexpr auto coro = [](bool &started) -> QCoro::LazyTask<int> {
+ started = true;
+ co_return 42;
+ };
+
+ bool started = false;
+ const auto task = coro(started);
+ QCORO_VERIFY(!started);
+
+ const auto result = co_await task;
+
+ QCORO_VERIFY(started);
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testNonVoidLazyCoroutineStarts_coro(QCoro::TestContext) {
+ constexpr auto coro = [](bool &started, bool &resumed) -> QCoro::LazyTask<int> {
+ started = true;
+ co_await QCoro::sleepFor(1ms);
+ resumed = true;
+ co_return 42;
+ };
+
+ bool started = false;
+ bool resumed = false;
+ const auto task = coro(started, resumed);
+ QCORO_VERIFY(!started);
+
+ const auto result = co_await task;
+
+ QCORO_VERIFY(started);
+ QCORO_VERIFY(resumed);
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testEagerInsideLazy_coro(QCoro::TestContext) {
+ constexpr auto coro = []() -> QCoro::LazyTask<int> {
+ co_return co_await []() -> QCoro::Task<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return 42;
+ }();
+ };
+
+ const auto task = coro();
+
+ const auto result = co_await task;
+
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testThenLazyContinuation_coro(QCoro::TestContext) {
+ constexpr auto coro = []() -> QCoro::LazyTask<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return 42;
+ };
+
+ const auto task = coro().then([](int result) -> QCoro::LazyTask<QString> {
+ co_await QCoro::sleepFor(1ms);
+ co_return QString::number(result);
+ });
+
+ const auto result = co_await task;
+ QCORO_COMPARE(result, QStringLiteral("42"));
+ }
+
+ QCoro::Task<> testThenEagerContinuation_coro(QCoro::TestContext) {
+ constexpr auto coro = []() -> QCoro::LazyTask<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return 42;
+ };
+
+ const auto task = coro().then([](int result) -> QCoro::Task<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return result;
+ });
+
+ const auto result = co_await task;
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testThenNonCoroutineContinuation_coro(QCoro::TestContext) {
+ constexpr auto coro = []() -> QCoro::LazyTask<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return 42;
+ };
+
+ const auto task = coro().then([](int result) {
+ return QString::number(result);
+ });
+ static_assert(std::is_same_v<decltype(task), const QCoro::LazyTask<QString>>);
+
+ const auto result = co_await task;
+ QCORO_COMPARE(result, QStringLiteral("42"));
+ }
+
+private Q_SLOTS:
+ addTest(SyncLazyCoroutineStarts)
+ addTest(LazyCoroutineStarts)
+ addTest(NonVoidSyncLazyCoroutineStarts)
+ addTest(NonVoidLazyCoroutineStarts)
+ addTest(EagerInsideLazy)
+ addTest(ThenLazyContinuation)
+ addTest(ThenEagerContinuation)
+ addTest(ThenNonCoroutineContinuation)
+
+ void testWaitFor() {
+ auto coro = []() -> QCoro::LazyTask<int> {
+ co_await QCoro::sleepFor(1ms);
+ co_return 42;
+ };
+
+ const auto result = QCoro::waitFor(coro());
+ QCOMPARE(result, 42);
+ }
+};
+
+
+QTEST_GUILESS_MAIN(QCoroLazyTaskTest)
+
+#include "qcorolazytask.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testhttpserver.h"
+#include "testobject.h"
+#include "qcoroiodevice_macros.h"
+#include "testloop.h"
+
+#include "qcoro/network/qcorolocalsocket.h"
+
+#include <QLocalServer>
+#include <QLocalSocket>
+
+#include <thread>
+
+static const QByteArray blockRequest = "GET /block HTTP/1.1\r\n";
+static const QByteArray streamRequest = "GET /stream HTTP/1.1\r\n";
+
+class QCoroLocalSocketTest : public QCoro::TestObject<QCoroLocalSocketTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testWaitForConnectedTriggers_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ QCORO_DELAY(socket.connectToServer(QCoroLocalSocketTest::getSocketName()));
+
+ co_await qCoro(socket).waitForConnected();
+
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForConnectedTriggers_coro(TestLoop &el) {
+ QLocalSocket socket;
+ QCORO_DELAY(socket.connectToServer(QCoroLocalSocketTest::getSocketName()));
+ bool called = false;
+ qCoro(socket).waitForConnected().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testWaitForDisconnectedTriggers_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ QCORO_DELAY(socket.disconnectFromServer());
+
+ co_await qCoro(socket).waitForDisconnected();
+
+ QCORO_COMPARE(socket.state(), QLocalSocket::UnconnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForDisconnectedTriggers_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ QCORO_DELAY(socket.disconnectFromServer());
+ bool called = false;
+ qCoro(socket).waitForDisconnected().then([&](bool disconnected) {
+ called = true;
+ el.quit();
+ QVERIFY(disconnected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ // On Linux at least, QLocalSocket connects immediately and synchronously
+ QCoro::Task<> testDoesntCoAwaitConnectedSocket_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ co_await qCoro(socket).waitForConnected();
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenDoesntCoAwaitConnectedSocket_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ bool called = false;
+ qCoro(socket).waitForConnected().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testDoesntCoAwaitDisconnectedSocket_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+ mServer.setExpectTimeout(true);
+
+ QLocalSocket socket;
+ QCORO_COMPARE(socket.state(), QLocalSocket::UnconnectedState);
+
+ co_await qCoro(socket).waitForDisconnected();
+ }
+
+ void testThenDoesntCoAwaitDisconnectedSocket_coro(TestLoop &el) {
+ mServer.setExpectTimeout(true);
+
+ QLocalSocket socket;
+ QCOMPARE(socket.state(), QLocalSocket::UnconnectedState);
+ bool called = false;
+ qCoro(socket).waitForDisconnected().then([&](bool disconnected) {
+ called = true;
+ el.quit();
+ QVERIFY(!disconnected);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testConnectToServerWithArgs_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ QLocalSocket socket;
+
+ co_await qCoro(socket).connectToServer(QCoroLocalSocketTest::getSocketName());
+
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenConnectToServerWithArgs_coro(TestLoop &el) {
+ QLocalSocket socket;
+ bool called = false;
+ qCoro(socket).connectToServer(QCoroLocalSocketTest::getSocketName()).then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testConnectToServer_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ QLocalSocket socket;
+ socket.setServerName(QCoroLocalSocketTest::getSocketName());
+
+ co_await qCoro(socket).connectToServer();
+
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenConnectToServer_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.setServerName(QCoroLocalSocketTest::getSocketName());
+ bool called = false;
+ qCoro(socket).connectToServer().then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testWaitForConnectedTimeout_coro(QCoro::TestContext) {
+ mServer.setExpectTimeout(true);
+
+ QLocalSocket socket;
+
+ QCORO_TEST_TIMEOUT(co_await qCoro(socket).waitForConnected(10ms));
+ }
+
+ void testThenWaitForConnectedTimeout_coro(TestLoop &el) {
+ mServer.setExpectTimeout(true);
+
+ QLocalSocket socket;
+
+ bool called = false;
+ qCoro(socket).waitForConnected(10ms).then([&](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(!connected);
+ });
+ const auto start = std::chrono::steady_clock::now();
+ el.exec();
+ const auto end = std::chrono::steady_clock::now();
+ QVERIFY(end - start < 500ms);
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testWaitForDisconnectedTimeout_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ QCORO_TEST_TIMEOUT(co_await qCoro(socket).waitForDisconnected(10ms));
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForDisconnectedTimeout_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+ bool called = false;
+ qCoro(socket).waitForDisconnected(10ms).then([&](bool disconnected) {
+ called = true;
+ el.quit();
+ QVERIFY(!disconnected);
+ });
+ const auto start = std::chrono::steady_clock::now();
+ el.exec();
+ const auto end = std::chrono::steady_clock::now();
+ QVERIFY(end - start < 500ms);
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadAllTriggers_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ const auto written = co_await qCoro(socket).write(streamRequest);
+ QCORO_COMPARE(written, streamRequest.size());
+
+ QCORO_TEST_IODEVICE_READALL(socket);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadAllTriggers_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+ bool called = false;
+ qCoro(socket).readAll().then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QVERIFY(!data.isEmpty());
+ });
+
+ qCoro(socket).write(blockRequest).then([&](qint64 written) {
+ QCOMPARE(written, blockRequest.size());
+ });
+ el.exec();
+
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadTriggers_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ const auto written = co_await qCoro(socket).write(streamRequest);
+ QCORO_COMPARE(written, streamRequest.size());
+
+ QCORO_TEST_IODEVICE_READ(socket);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadTriggers_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+ bool called = false;
+ qCoro(socket).read(1).then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QCOMPARE(data.size(), 1);
+ });
+
+ qCoro(socket).write(blockRequest).then([&](qint64 written) {
+ QCOMPARE(written, blockRequest.size());
+ });
+
+ el.exec();
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testReadLineTriggers_coro(QCoro::TestContext) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCORO_COMPARE(socket.state(), QLocalSocket::ConnectedState);
+
+ const auto written = co_await qCoro(socket).write(streamRequest);
+ QCORO_COMPARE(written, streamRequest.size());
+
+ QCORO_TEST_IODEVICE_READLINE(socket);
+ QCORO_COMPARE(lines.size(), 14);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenReadLineTriggers_coro(TestLoop &el) {
+ QLocalSocket socket;
+ socket.connectToServer(QCoroLocalSocketTest::getSocketName());
+ QCOMPARE(socket.state(), QLocalSocket::ConnectedState);
+ bool called = false;
+ qCoro(socket).readLine().then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QVERIFY(!data.isEmpty());
+ });
+
+ qCoro(socket).write(blockRequest).then([&](qint64 written) {
+ QCOMPARE(written, blockRequest.size());
+ });
+
+ el.exec();
+
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+private Q_SLOTS:
+ void init() {
+ mServer.start(QCoroLocalSocketTest::getSocketName());
+ }
+
+ void cleanup() {
+ mServer.stop();
+ }
+
+ addCoroAndThenTests(WaitForConnectedTriggers)
+ addCoroAndThenTests(WaitForConnectedTimeout)
+ addCoroAndThenTests(WaitForDisconnectedTriggers)
+ addCoroAndThenTests(WaitForDisconnectedTimeout)
+ addCoroAndThenTests(DoesntCoAwaitConnectedSocket)
+ addCoroAndThenTests(DoesntCoAwaitDisconnectedSocket)
+ addCoroAndThenTests(ConnectToServerWithArgs)
+ addCoroAndThenTests(ConnectToServer)
+ addCoroAndThenTests(ReadAllTriggers)
+ addCoroAndThenTests(ReadTriggers)
+ addCoroAndThenTests(ReadLineTriggers)
+
+private:
+ static QString getSocketName() {
+
+ return QStringLiteral("%1-%2")
+ .arg(QCoreApplication::applicationName())
+ .arg(QCoreApplication::applicationPid());
+ }
+
+ TestHttpServer<QLocalServer> mServer;
+};
+
+QTEST_GUILESS_MAIN(QCoroLocalSocketTest)
+
+#include "qcorolocalsocket.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testhttpserver.h"
+#include "testobject.h"
+#include "qcoroiodevice_macros.h"
+
+#include "qcoro/network/qcoronetworkreply.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QTcpServer>
+
+class QCoroNetworkReplyTest : public QCoro::TestObject<QCoroNetworkReplyTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(nam.get(buildRequest()));
+
+ co_await reply.get();
+
+ QCORO_VERIFY(reply->isFinished());
+ QCORO_COMPARE(reply->error(), QNetworkReply::NoError);
+ QCORO_COMPARE(reply->readAll(), "abcdef");
+ }
+
+ QCoro::Task<> testQCoroWrapperTriggers_coro(QCoro::TestContext) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(nam.get(buildRequest()));
+
+ co_await qCoro(reply.get()).waitForFinished();
+
+ QCORO_VERIFY(reply->isFinished());
+ QCORO_COMPARE(reply->error(), QNetworkReply::NoError);
+ QCORO_COMPARE(reply->readAll(), "abcdef");
+ }
+
+ void testThenQCoroWrapperTriggers_coro(TestLoop &el) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(nam.get(buildRequest()));
+
+ bool called = false;
+ qCoro(reply.get()).waitForFinished().then([&](bool finished) {
+ called = true;
+ el.quit();
+ QVERIFY(finished);
+ });
+ el.exec();
+ QVERIFY(reply->isFinished());
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+ QCOMPARE(reply->readAll(), "abcdef");
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testDoesntBlockEventLoop_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive;
+ QNetworkAccessManager nam;
+
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("block"))));
+
+ co_await reply.get();
+
+ QCORO_VERIFY(eventLoopResponsive);
+ QCORO_VERIFY(reply->isFinished());
+ QCORO_COMPARE(reply->error(), QNetworkReply::NoError);
+ QCORO_COMPARE(reply->readAll(), "abcdef");
+ }
+
+ QCoro::Task<> testDoesntCoAwaitNullReply_coro(QCoro::TestContext test) {
+ test.setShouldNotSuspend();
+ mServer.setExpectTimeout(true);
+
+ QNetworkReply *reply = nullptr;
+
+ co_await reply;
+
+ delete reply;
+ }
+
+ QCoro::Task<> testDoesntCoAwaitFinishedReply_coro(QCoro::TestContext test) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(nam.get(buildRequest()));
+
+ co_await reply.get();
+
+ QCORO_VERIFY(reply->isFinished());
+
+ test.setShouldNotSuspend();
+ co_await reply.get();
+ }
+
+ QCoro::Task<> testReadAllTriggers_coro(QCoro::TestContext) {
+ QNetworkAccessManager nam;
+
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("stream"))));
+
+ QCORO_TEST_IODEVICE_READALL(*reply);
+
+ QCORO_COMPARE(data.size(), reply->rawHeader("Content-Length").toInt());
+ }
+
+ void testThenReadAllTriggers_coro(TestLoop &el) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("block"))));
+
+ bool called = false;
+ qCoro(reply.get()).readAll().then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QVERIFY(!data.isEmpty());
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testReadTriggers_coro(QCoro::TestContext) {
+ QNetworkAccessManager nam;
+
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("stream"))));
+
+ QCORO_TEST_IODEVICE_READ(*reply);
+
+ QCORO_COMPARE(data.size(), reply->rawHeader("Content-Length").toInt());
+ }
+
+ void testThenReadTriggers_coro(TestLoop &el) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("block"))));
+ bool called = false;
+ qCoro(reply.get()).read(1).then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QCOMPARE(data.size(), 1);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testReadLineTriggers_coro(QCoro::TestContext) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("stream"))));
+
+ QCORO_TEST_IODEVICE_READLINE(*reply);
+ QCORO_COMPARE(lines.size(), 10);
+ }
+
+ void testThenReadLineTriggers_coro(TestLoop &el) {
+ QNetworkAccessManager nam;
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(buildRequest(QStringLiteral("block"))));
+ bool called = false;
+ qCoro(reply.get()).readLine().then([&](const QByteArray &data) {
+ called = true;
+ el.quit();
+ QVERIFY(!data.isEmpty());
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ // See https://github.com/danvratil/qcoro/issues/231
+ QCoro::Task<> testAbortOnTimeout_coro(QCoro::TestContext) {
+ auto request = buildRequest(QStringLiteral("block"));
+ request.setTransferTimeout(300);
+ QNetworkAccessManager nam;
+ auto reply = co_await nam.get(request);
+ QCORO_VERIFY(reply != nullptr);
+ QCORO_VERIFY(reply->isFinished());
+ QCORO_COMPARE(reply->error(), QNetworkReply::OperationCanceledError);
+ // QNAM is destroyed here and so is all its associated state, which could
+ // crash (or cause invalid memory access)
+ }
+
+private Q_SLOTS:
+ void init() {
+ mServer.start(QHostAddress::LocalHost);
+ }
+
+ void cleanup() {
+ mServer.stop();
+ }
+
+ addTest(Triggers)
+ addCoroAndThenTests(QCoroWrapperTriggers)
+ addTest(DoesntBlockEventLoop)
+ addTest(DoesntCoAwaitNullReply)
+ addTest(DoesntCoAwaitFinishedReply)
+ addCoroAndThenTests(ReadAllTriggers)
+ addCoroAndThenTests(ReadTriggers)
+ addCoroAndThenTests(ReadLineTriggers)
+ addTest(AbortOnTimeout)
+
+private:
+ QNetworkRequest buildRequest(const QString &path = QString()) {
+ return QNetworkRequest{
+ QUrl{QStringLiteral("http://127.0.0.1:%1/%2").arg(mServer.port()).arg(path)}
+ };
+ }
+
+ TestHttpServer<QTcpServer> mServer;
+};
+
+QTEST_GUILESS_MAIN(QCoroNetworkReplyTest)
+
+#include "qcoronetworkreply.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcoro/core/qcoroprocess.h"
+
+#include <QProcess>
+
+#ifdef Q_OS_WIN
+// There's no equivalent to "true" command on Windows, so do a single ping to localhost instead,
+// which terminates almost immediately.
+#define DUMMY_EXEC QStringLiteral("ping")
+#define DUMMY_ARGS \
+ { QStringLiteral("127.0.0.1"), QStringLiteral("-n"), QStringLiteral("1") }
+// On windows, the equivalent to Linux "sleep" is "timeout", but it fails due to QProcess redirecting
+// stdin, which "timeout" doesn't support (it waits for keypress to interrupt). However, "ping" pings
+// every second, so specifying number of pings to the desired timeout makes it behave basically like
+// the Linux "sleep".
+#define SLEEP_EXEC QStringLiteral("ping")
+#define SLEEP_ARGS(timeout) \
+ { QStringLiteral("127.0.0.1"), QStringLiteral("-n"), QString::number(timeout) }
+
+#else
+#define DUMMY_EXEC QStringLiteral("true")
+#define DUMMY_ARGS {}
+#define SLEEP_EXEC QStringLiteral("sleep")
+#define SLEEP_ARGS(timeout) { QString::number(timeout) }
+#endif
+
+class QCoroProcessTest : public QCoro::TestObject<QCoroProcessTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testStartTriggers_coro(QCoro::TestContext context) {
+#ifdef Q_OS_WIN
+ // QProcess::start() on Windows is synchronous, despite what the documentation says,
+ // so the coroutine will not suspend.
+ context.setShouldNotSuspend();
+#else
+ Q_UNUSED(context);
+#endif
+
+ QProcess process;
+ const bool ok = co_await qCoro(process).start(DUMMY_EXEC, DUMMY_ARGS);
+
+ QCORO_VERIFY(ok);
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ process.waitForFinished();
+ }
+
+ void testThenStartTriggers_coro(TestLoop &el) {
+ QProcess process;
+ bool called = false;
+ qCoro(process).start(DUMMY_EXEC, DUMMY_ARGS).then([&](bool started) {
+ called = true;
+ el.quit();
+ QVERIFY(started);
+ QCOMPARE(process.state(), QProcess::Running);
+ });
+ el.exec();
+ QVERIFY(called);
+
+ process.waitForFinished();
+ }
+
+ QCoro::Task<> testStartNoArgsTriggers_coro(QCoro::TestContext context) {
+#ifdef Q_OS_WIN
+ context.setShouldNotSuspend();
+#else
+ Q_UNUSED(context);
+#endif
+
+ QProcess process;
+ process.setProgram(DUMMY_EXEC);
+ process.setArguments(DUMMY_ARGS);
+
+ const bool ok = co_await qCoro(process).start();
+
+ QCORO_VERIFY(ok);
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ process.waitForFinished();
+ }
+
+ void testThenStartNoArgsTriggers_coro(TestLoop &el) {
+ QProcess process;
+ process.setProgram(DUMMY_EXEC);
+ process.setArguments(DUMMY_ARGS);
+
+ bool called = false;
+ qCoro(process).start().then([&](bool started) {
+ called = true;
+ el.quit();
+ QVERIFY(started);
+ QCOMPARE(process.state(), QProcess::Running);
+ });
+ el.exec();
+ QVERIFY(called);
+
+ process.waitForFinished();
+ }
+
+ QCoro::Task<> testStartDoesntBlock_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive{1, 0ms};
+
+ QProcess process;
+ const bool ok = co_await qCoro(process).start(DUMMY_EXEC, DUMMY_ARGS);
+
+ QCORO_VERIFY(ok);
+ QCORO_VERIFY(eventLoopResponsive);
+
+ process.waitForFinished();
+ }
+
+ QCoro::Task<> testStartDoesntCoAwaitRunningProcess_coro(QCoro::TestContext ctx) {
+ QProcess process;
+#if defined(__GNUC__) && !defined(__clang__)
+#pragma message "Workaround for GCC ICE!"
+ // Workaround GCC bug https://bugzilla.redhat.com/1952671
+ // GCC ICEs at the end of this function due to presence of two co_await statements.
+ process.start(SLEEP_EXEC, SLEEP_ARGS(1));
+ process.waitForStarted();
+#else
+ const bool ok = co_await qCoro(process).start(SLEEP_EXEC, SLEEP_ARGS(1));
+ QCORO_VERIFY(ok);
+#endif
+
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ ctx.setShouldNotSuspend();
+
+ QTest::ignoreMessage(QtWarningMsg, "QProcess::start: Process is already running");
+ co_await qCoro(process).start();
+
+ process.waitForFinished();
+ }
+
+ QCoro::Task<> testFinishTriggers_coro(QCoro::TestContext) {
+ QProcess process;
+ process.start(SLEEP_EXEC, SLEEP_ARGS(1));
+ process.waitForStarted();
+
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ const auto ok = co_await qCoro(process).waitForFinished();
+
+ QCORO_VERIFY(ok);
+ QCORO_COMPARE(process.state(), QProcess::NotRunning);
+ }
+
+ void testThenFinishTriggers_coro(TestLoop &el) {
+ QProcess process;
+ process.start(SLEEP_EXEC, SLEEP_ARGS(1));
+ process.waitForStarted();
+
+ QCOMPARE(process.state(), QProcess::Running);
+
+ bool called = false;
+ qCoro(process).waitForFinished().then([&](bool finished) {
+ called = true;
+ el.quit();
+ QVERIFY(finished);
+
+ QCOMPARE(process.state(), QProcess::NotRunning);
+ QCOMPARE(process.exitStatus(), QProcess::NormalExit);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testFinishDoesntCoAwaitFinishedProcess_coro(QCoro::TestContext ctx) {
+ QProcess process;
+ process.start(DUMMY_EXEC, QStringList DUMMY_ARGS);
+ process.waitForFinished();
+
+ ctx.setShouldNotSuspend();
+
+ const auto ok = co_await qCoro(process).waitForFinished();
+ QCORO_VERIFY(!ok);
+ }
+
+ QCoro::Task<> testFinishCoAwaitTimeout_coro(QCoro::TestContext) {
+ QProcess process;
+
+ process.start(SLEEP_EXEC, SLEEP_ARGS(2));
+ process.waitForStarted();
+
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ const auto ok = co_await qCoro(process).waitForFinished(1s);
+
+ QCORO_VERIFY(!ok);
+ QCORO_COMPARE(process.state(), QProcess::Running);
+
+ process.waitForFinished();
+ }
+
+ void testThenFinishCoAwaitTimeout_coro(TestLoop &el) {
+ QProcess process;
+ process.start(SLEEP_EXEC, SLEEP_ARGS(2));
+ process.waitForStarted();
+
+ QCOMPARE(process.state(), QProcess::Running);
+ bool called = false;
+ qCoro(process).waitForFinished(1s).then([&](bool finished) {
+ called = true;
+ el.quit();
+ QVERIFY(!finished);
+ });
+ el.exec();
+ QVERIFY(called);
+
+ process.waitForFinished();
+ }
+
+private Q_SLOTS:
+ addCoroAndThenTests(StartTriggers)
+ addCoroAndThenTests(StartNoArgsTriggers)
+#ifndef Q_OS_WIN // start always blocks on Windows
+ addTest(StartDoesntBlock)
+#endif
+ addTest(StartDoesntCoAwaitRunningProcess)
+ addCoroAndThenTests(FinishTriggers)
+ addTest(FinishDoesntCoAwaitFinishedProcess)
+ addCoroAndThenTests(FinishCoAwaitTimeout)
+};
+
+QTEST_GUILESS_MAIN(QCoroProcessTest)
+
+#include "qcoroprocess.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcoroqml.h"
+#include "qcorotask.h"
+#include "qcorotimer.h"
+#include "qcoroqmltask.h"
+#include "qcorofuture.h"
+
+#include <QTest>
+#include <QTimer>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+class QmlObject : public QObject {
+ Q_OBJECT
+
+public:
+ Q_INVOKABLE QCoro::QmlTask startTimer() {
+ auto *timer = new QTimer(this);
+ timer->setSingleShot(true);
+ timer->start(1s);
+ return [timer]() -> QCoro::Task<> {
+ co_await timer;
+ }();
+ }
+
+ Q_INVOKABLE QCoro::QmlTask qmlTaskFromTimer() {
+ auto *timer = new QTimer(this);
+ timer->setSingleShot(true);
+ timer->start(1s);
+ return timer;
+ }
+
+ Q_INVOKABLE QCoro::QmlTask qmlTaskFromFuture() {
+ QFutureInterface<QString> interface;
+ interface.reportResult(QStringLiteral("Success"));
+ interface.reportFinished();
+ return interface.future();
+ }
+
+ Q_INVOKABLE void reportTestSuccess() {
+ numTestsPassed++;
+
+ if (numTestsPassed == 4) { // Number of java script functions that call reportTestSuccess
+ Q_EMIT success();
+ }
+ }
+
+ Q_SIGNAL void success();
+
+private:
+ int numTestsPassed = 0;
+};
+
+class QCoroQmlTaskTest : public QObject {
+ Q_OBJECT
+
+private:
+ Q_SLOT void testQmlCallback() {
+ QQmlApplicationEngine engine;
+ qmlRegisterSingletonType<QmlObject>("qcoro.test", 0, 1, "QmlObject", [](QQmlEngine *, QJSEngine *) {
+ return new QmlObject();
+ });
+
+ QCoro::Qml::registerTypes();
+
+ engine.loadData(R"(
+import qcoro.test 0.1
+import QCoro 0
+import QtQuick 2.7
+
+QtObject {
+ property string value: QmlObject.qmlTaskFromFuture().await("Loading...").value
+
+ property string valueWithoutIntermediate: QmlObject.qmlTaskFromFuture().await().value
+
+ onValueChanged: {
+ if (value == "Success") {
+ console.log("awaiting finished")
+ QmlObject.reportTestSuccess()
+ }
+ }
+
+ Component.onCompleted: {
+ QmlObject.startTimer().then(() => {
+ console.log("QCoro::Task JavaScript callback called")
+ QmlObject.reportTestSuccess()
+ })
+
+ QmlObject.qmlTaskFromTimer().then(() => {
+ console.log("QTimer JavaScript callback called")
+ QmlObject.reportTestSuccess()
+ })
+
+ QmlObject.qmlTaskFromFuture().then(() => {
+ console.log("QFuture JavaScript callback called")
+ QmlObject.reportTestSuccess()
+ })
+ }
+}
+)");
+ auto *object = engine.singletonInstance<QmlObject *>(qmlTypeId("qcoro.test", 0, 1, "QmlObject"));
+
+ auto *timeout = new QTimer(this);
+ timeout->setSingleShot(true);
+ timeout->setInterval(2s);
+ timeout->start();
+
+ bool running = true;
+ // End the event loop normally
+ connect(object, &QmlObject::success, this, [&]() {
+ timeout->stop();
+ running = false;
+ });
+
+ // Crash the test in case the timeout was reachaed without the callback being called
+ connect(timeout, &QTimer::timeout, this, [&]() {
+#if defined(Q_CC_CLANG) && defined(Q_OS_WINDOWS)
+ running = false;
+ QEXPECT_FAIL("", "QTBUG-91768", Abort);
+ QVERIFY(false);
+ return;
+#else
+ QFAIL("Timeout waiting for QML continuation to be called");
+#endif
+ });
+ while (running) {
+ QCoreApplication::processEvents();
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(QCoroQmlTaskTest)
+
+#include "qcoroqmltask.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcoro/core/qcorotimer.h"
+#include "qcoro/core/qcorosignal.h"
+
+#include <QTimer>
+#include <QThread>
+
+using namespace std::chrono_literals;
+
+class SignalTest : public QObject {
+ Q_OBJECT
+public:
+ explicit SignalTest(bool active = true) {
+ if (active) {
+ QTimer::singleShot(100ms, this, &SignalTest::emit);
+ }
+ }
+
+ void emit() {
+ Q_EMIT voidSignal();
+ Q_EMIT singleArg(QStringLiteral("YAY!"));
+ Q_EMIT multiArg(QStringLiteral("YAY!"), 42, this);
+ Q_EMIT privateVoid(QPrivateSignal{});
+ Q_EMIT privateSingleArg(QStringLiteral("YAY!"), QPrivateSignal{});
+ Q_EMIT privateMultiArg(QStringLiteral("YAY!"), 42, this, QPrivateSignal{});
+ }
+
+Q_SIGNALS:
+ void voidSignal();
+ void singleArg(const QString &);
+ void multiArg(const QString &, int, QObject *);
+ void privateVoid(QPrivateSignal);
+ void privateSingleArg(const QString &, QPrivateSignal);
+ void privateMultiArg(const QString &, int, QObject *, QPrivateSignal);
+
+ void signalThatsNeverEmitted();
+};
+
+class MultiSignalTest : public SignalTest {
+ Q_OBJECT
+public:
+ explicit MultiSignalTest(bool active = true)
+ : SignalTest(false) {
+ if (active) {
+ mTimer.setInterval(10ms);
+ connect(&mTimer, &QTimer::timeout, this, &MultiSignalTest::emit);
+ mTimer.start();
+ }
+ }
+
+private:
+ QTimer mTimer;
+};
+
+
+class SimpleSignal: public QObject {
+ Q_OBJECT
+public:
+ void send(int id) {
+ Q_EMIT messageReceived(id);
+ }
+
+ Q_SIGNAL void messageReceived(int id);
+
+ QCoro::Task<int> waitForMessage(int id) {
+ QCORO_FOREACH(int msgId, qCoroSignalListener(this, &SimpleSignal::messageReceived)) {
+ if (msgId == id) {
+ co_return id;
+ }
+ }
+
+ co_return -1;
+ }
+};
+
+class QCoroSignalTest : public QCoro::TestObject<QCoroSignalTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ co_await qCoro(&obj, &SignalTest::voidSignal);
+ static_assert(
+ std::is_same_v<decltype(qCoro(&obj, &SignalTest::voidSignal)), QCoro::Task<std::tuple<>>>);
+ }
+
+ QCoro::Task<> testReturnsValue_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::singleArg);
+ static_assert(std::is_same_v<decltype(result), const QString>);
+ QCORO_COMPARE(result, QStringLiteral("YAY!"));
+ }
+
+ QCoro::Task<> testReturnsTuple_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::multiArg);
+ static_assert(std::is_same_v<decltype(result), const std::tuple<QString, int, QObject *>>);
+ const auto [value, number, ptr] = result;
+ QCORO_COMPARE(value, QStringLiteral("YAY!"));
+ QCORO_COMPARE(number, 42);
+ QCORO_COMPARE(ptr, &obj);
+ }
+
+ QCoro::Task<> testTimeoutTriggersVoid_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::voidSignal, 10ms);
+ static_assert(std::is_same_v<decltype(result), const std::optional<std::tuple<>>>);
+ QCORO_VERIFY(!result.has_value());
+ }
+
+ QCoro::Task<> testTimeoutVoid_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::voidSignal, 1s);
+ static_assert(std::is_same_v<decltype(result), const std::optional<std::tuple<>>>);
+ QCORO_VERIFY(result.has_value());
+ }
+
+ QCoro::Task<> testTimeoutTriggersValue_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::singleArg, 10ms);
+ static_assert(std::is_same_v<decltype(result), const std::optional<QString>>);
+ QCORO_VERIFY(!result.has_value());
+ }
+
+ QCoro::Task<> testTimeoutValue_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::singleArg, 1s);
+ static_assert(std::is_same_v<decltype(result), const std::optional<QString>>);
+ QCORO_VERIFY(result.has_value());
+ QCORO_COMPARE(*result, QStringLiteral("YAY!"));
+ }
+
+ QCoro::Task<> testTimeoutTriggersTuple_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::multiArg, 10ms);
+ static_assert(std::is_same_v<
+ decltype(result),
+ const std::optional<std::tuple<QString, int, QObject *>>>);
+ QCORO_VERIFY(!result.has_value());
+ }
+
+ QCoro::Task<> testTimeoutTuple_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::multiArg, 1s);
+ static_assert(std::is_same_v<
+ decltype(result),
+ const std::optional<std::tuple<QString, int, QObject *>>>);
+ QCORO_VERIFY(result.has_value());
+ QCORO_COMPARE(std::get<0>(*result), QStringLiteral("YAY!"));
+ QCORO_COMPARE(std::get<1>(*result), 42);
+ QCORO_COMPARE(std::get<2>(*result), &obj);
+ }
+
+
+ void testThenTriggers_coro(TestLoop &el) {
+ SignalTest obj;
+ bool triggered = false;
+
+ qCoro(&obj, &SignalTest::voidSignal).then([&]() {
+ triggered = true;
+ el.quit();
+ });
+ el.exec();
+
+ QVERIFY(triggered);
+ }
+
+ void testThenReturnsValue_coro(TestLoop &el) {
+ SignalTest obj;
+ std::optional<QString> value;
+
+ qCoro(&obj, &SignalTest::singleArg).then([&](const QString &arg) {
+ value = arg;
+ el.quit();
+ });
+ el.exec();
+
+ QVERIFY(value.has_value());
+ QCOMPARE(*value, QStringLiteral("YAY!"));
+ }
+
+ void testThenReturnsTuple_coro(TestLoop &el) {
+ SignalTest obj;
+ std::optional<QString> str;
+ std::optional<int> num;
+ std::optional<QObject *> ptr;
+
+ qCoro(&obj, &SignalTest::multiArg).then([&](const std::tuple<QString, int, QObject *> &args) {
+ str = std::get<0>(args);
+ num = std::get<1>(args);
+ ptr = std::get<2>(args);
+ el.quit();
+ });
+ el.exec();
+
+ QVERIFY(str.has_value());
+ QVERIFY(num.has_value());
+ QVERIFY(ptr.has_value());
+ QCOMPARE(*str, QStringLiteral("YAY!"));
+ QCOMPARE(*num, 42);
+ QCOMPARE(*ptr, &obj);
+ }
+
+ QCoro::Task<> testThenChained_coro(QCoro::TestContext) {
+ SignalTest obj;
+ const auto result = co_await qCoro(&obj, &SignalTest::singleArg).then([](const QString &arg) -> QCoro::Task<QString> {
+ QTimer timer;
+ timer.start(100ms);
+ co_await timer;
+ co_return arg + arg;
+ });
+
+ QCORO_COMPARE(result, QStringLiteral("YAY!YAY!"));
+ }
+
+ QCoro::Task<> testVoidQPrivateSignal_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::privateVoid);
+ static_assert(std::is_same_v<decltype(result), const std::tuple<>>);
+ Q_UNUSED(result);
+ }
+
+ QCoro::Task<> testSingleArgQPrivateSignal_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto result = co_await qCoro(&obj, &SignalTest::privateSingleArg);
+ static_assert(std::is_same_v<decltype(result), const QString>);
+ QCORO_COMPARE(result, QStringLiteral("YAY!"));
+ }
+
+ QCoro::Task<> testMultiArgQPrivateSignal_coro(QCoro::TestContext) {
+ SignalTest obj;
+
+ const auto [str, num, ptr] = co_await qCoro(&obj, &SignalTest::privateMultiArg);
+ static_assert(std::is_same_v<decltype(str), const QString>);
+ static_assert(std::is_same_v<decltype(num), const int>);
+ static_assert(std::is_same_v<decltype(ptr), QObject * const>);
+ QCORO_COMPARE(str, QStringLiteral("YAY!"));
+ QCORO_COMPARE(num, 42);
+ QCORO_COMPARE(ptr, &obj);
+ }
+
+ QCoro::Task<> testSignalListenerVoid_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::voidSignal);
+ int count = 0;
+ QCORO_FOREACH(const std::tuple<> &value, generator) {
+ Q_UNUSED(value);
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalListenerValue_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::singleArg);
+ int count = 0;
+ QCORO_FOREACH(const QString &value, generator) {
+ QCORO_COMPARE(value, QStringLiteral("YAY!"));
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalListenerTuple_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::multiArg);
+ int count = 0;
+ QCORO_FOREACH(const auto &value, generator) {
+ QCORO_COMPARE(std::get<0>(value), QStringLiteral("YAY!"));
+ QCORO_COMPARE(std::get<1>(value), 42);
+ QCORO_COMPARE(std::get<2>(value), &obj);
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalListenerTimeout_coro(QCoro::TestContext) {
+ QObject obj;
+
+ // A signal that doesn't get invoked
+ auto generator = qCoroSignalListener(&obj, &QObject::destroyed, 1ms);
+ QCORO_FOREACH(const auto &value, generator) {
+ Q_UNUSED(value);
+ QCORO_FAIL("The signal should time out and the generator should not return invalid iterator.");
+ }
+ }
+
+ QCoro::Task<> testSignalListenerQueue_coro(QCoro::TestContext ctx) {
+ SignalTest test{false};
+ // I have a generator
+ auto generator = qCoroSignalListener(&test, &SignalTest::voidSignal);
+ // I emit signals that the generator is listening to, the generator
+ // should enqueue them.
+ for (int i = 0; i < 10; ++i) {
+ test.emit();
+ }
+
+ // I asynchronously wait for first iterator
+ auto it = co_await generator.begin();
+ int count = 0;
+ ctx.setShouldNotSuspend();
+ // I loop over generator - this should not suspend as we are simply consuming
+ // events from the queue.
+ for (; it != generator.end(); co_await ++it) {
+ if (++count == 10) {
+ break;
+ }
+ }
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalAfterListenerQuits_coro(QCoro::TestContext) {
+ SimpleSignal simple;
+ auto msg1 = simple.waitForMessage(1);
+ auto msg2 = simple.waitForMessage(2);
+ simple.send(1);
+ simple.send(2);
+ QCORO_COMPARE(co_await msg1, 1);
+ QCORO_COMPARE(co_await msg2, 2);
+ }
+
+ QCoro::Task<> testSignalListenerQPrivateSignalVoid_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::privateVoid);
+ int count = 0;
+ QCORO_FOREACH(const auto &value, generator) {
+ static_assert(std::is_same_v<decltype(value), const std::tuple<> &>);
+ Q_UNUSED(value);
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalListenerQPrivateSignalValue_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::privateSingleArg);
+ int count = 0;
+ QCORO_FOREACH(const auto &value, generator) {
+ static_assert(std::is_same_v<decltype(value), const QString &>);
+ QCORO_COMPARE(value, QStringLiteral("YAY!"));
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalListenerQPrivateSignalTuple_coro(QCoro::TestContext) {
+ MultiSignalTest obj;
+
+ auto generator = qCoroSignalListener(&obj, &MultiSignalTest::privateMultiArg);
+ int count = 0;
+ QCORO_FOREACH(const auto &value, generator) {
+ static_assert(std::is_same_v<decltype(value), const std::tuple<QString, int, QObject *> &>);
+ QCORO_COMPARE(std::get<QString>(value), QStringLiteral("YAY!"));
+ QCORO_COMPARE(std::get<int>(value), 42);
+ QCORO_COMPARE(std::get<QObject *>(value), &obj);
+ if (++count == 10) {
+ break;
+ }
+ }
+
+ QCORO_COMPARE(count, 10);
+ }
+
+ QCoro::Task<> testSignalEmitterOnDifferentThread_coro(QCoro::TestContext) {
+ SignalTest test;
+ QThread thread;
+ test.moveToThread(&thread);
+ thread.start();
+
+ co_await qCoro(&test, &SignalTest::voidSignal);
+ // Make sure we are resumed on our thread
+ QCORO_COMPARE(QThread::currentThread(), qApp->thread());
+
+ co_await qCoro(&test, &SignalTest::signalThatsNeverEmitted, 20ms);
+ // Make sure we are resumed on our thread after the timeout
+ QCORO_COMPARE(QThread::currentThread(), qApp->thread());
+
+ thread.quit();
+ thread.wait();
+ }
+
+private Q_SLOTS:
+ addTest(Triggers)
+ addTest(ReturnsValue)
+ addTest(ReturnsTuple)
+ addTest(TimeoutVoid)
+ addTest(TimeoutTriggersVoid)
+ addTest(TimeoutValue)
+ addTest(TimeoutTriggersValue)
+ addTest(TimeoutTuple)
+ addTest(TimeoutTriggersTuple)
+ addTest(ThenChained)
+ addTest(VoidQPrivateSignal)
+ addTest(SingleArgQPrivateSignal)
+ addTest(MultiArgQPrivateSignal)
+ addThenTest(Triggers)
+ addThenTest(ReturnsValue)
+ addThenTest(ReturnsTuple)
+ addTest(SignalListenerVoid)
+ addTest(SignalListenerValue)
+ addTest(SignalListenerTuple)
+ addTest(SignalListenerTimeout)
+ addTest(SignalListenerQueue)
+ addTest(SignalAfterListenerQuits)
+ addTest(SignalListenerQPrivateSignalVoid)
+ addTest(SignalListenerQPrivateSignalValue)
+ addTest(SignalListenerQPrivateSignalTuple)
+ addTest(SignalEmitterOnDifferentThread)
+};
+
+QTEST_GUILESS_MAIN(QCoroSignalTest)
+
+#include "qcorosignal.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+#include "qcorotask.h"
+#include "qcorotimer.h"
+#include "qcorosignal.h"
+
+#include <QTest>
+#include <QObject>
+#include <QScopeGuard>
+#include <QMetaObject>
+#include <QTimer>
+#include <QElapsedTimer>
+
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+namespace {
+
+QCoro::Task<> timer(std::chrono::milliseconds timeout = 10ms) {
+ QTimer timer;
+ timer.setSingleShot(true);
+ timer.start(timeout);
+ co_await timer;
+}
+
+template<typename T>
+QCoro::Task<T> timerWithValue(T value, std::chrono::milliseconds timeout = 10ms) {
+ co_await timer(timeout);
+ co_return value;
+}
+
+auto thenScopeTestFunc(QEventLoop *el) {
+ return timer().then([el]() {
+ el->quit();
+ });
+}
+
+template<typename T>
+QCoro::Task<T> thenScopeTestFuncWithValue(T value) {
+ return timer().then([value]() {
+ return value;
+ });
+}
+
+class ImplicitConversionBar {
+public:
+ int number;
+};
+class ImplicitConversionFoo {
+public:
+ ImplicitConversionFoo();
+ ImplicitConversionFoo(ImplicitConversionBar bar)
+ : string(QString::number(bar.number)) {}
+
+ QString string;
+};
+
+struct TestAwaitableBase {
+ std::chrono::milliseconds delay() const { return mDelay; }
+private:
+ std::chrono::milliseconds mDelay = 100ms;
+};
+
+template<typename T>
+struct TestAwaitable : TestAwaitableBase {
+public:
+ TestAwaitable(T val)
+ : mResult(val)
+ {}
+
+ bool await_ready() const { return false; }
+ void await_suspend(std::coroutine_handle<> handle) {
+ QTimer::singleShot(100ms, [handle = std::move(handle)]() {
+ handle.resume();
+ });
+ }
+ T await_resume() {
+ return mResult;
+ }
+
+private:
+ T mResult;
+};
+
+template<>
+struct TestAwaitable<void> : TestAwaitableBase {
+public:
+ bool await_ready() const { return false; }
+ void await_suspend(std::coroutine_handle<> handle) {
+ QTimer::singleShot(100ms, [handle = std::move(handle)]() {
+ handle.resume();
+ });
+ }
+ void await_resume() {}
+};
+
+template<typename T>
+struct TestAwaitableWithCoAwait {
+ TestAwaitableWithCoAwait(T val)
+ : mResult(val)
+ {}
+
+ TestAwaitable<T> operator co_await() {
+ return TestAwaitable(mResult);
+ }
+
+private:
+ T mResult;
+};
+
+template<>
+struct TestAwaitableWithCoAwait<void> {
+ TestAwaitable<void> operator co_await() {
+ return TestAwaitable<void>();
+ }
+};
+
+} // namespace
+
+class QCoroTaskTest : public QCoro::TestObject<QCoroTaskTest>
+{
+ Q_OBJECT
+
+private:
+ template<typename Coro>
+ void ignoreCoroutineResult(QEventLoop &el, Coro &&coro) {
+ QTimer::singleShot(5s, &el, [&el]() mutable { el.exit(1); });
+
+ coro();
+ const int timeout = el.exec();
+ QCOMPARE(timeout, 0);
+ }
+
+ QCoro::Task<> testSimpleCoroutine_coro(QCoro::TestContext) {
+ co_await timer();
+ }
+
+ QCoro::Task<> testCoroutineValue_coro(QCoro::TestContext) {
+ const auto coro = [](const QString &result) -> QCoro::Task<QString> {
+ co_await timer();
+ co_return result;
+ };
+
+ const auto value = QStringLiteral("Done!");
+ const auto result = co_await coro(value);
+ QCORO_COMPARE(result, value);
+ }
+
+ QCoro::Task<> testCoroutineMoveValue_coro(QCoro::TestContext) {
+ const auto coro = [](const QString &result) -> QCoro::Task<std::unique_ptr<QString>> {
+ co_await timer();
+ co_return std::make_unique<QString>(result);
+ };
+
+ const auto value = QStringLiteral("Done ptr!");
+ const auto result = co_await coro(value);
+ QCORO_COMPARE(*result.get(), value);
+ }
+
+ QCoro::Task<> testSyncCoroutine_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ const auto coro = []() -> QCoro::Task<int> {
+ co_return 42;
+ };
+
+ const auto result = co_await coro();
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testCoroutineWithException_coro(QCoro::TestContext) {
+ const auto coro = []() -> QCoro::Task<int> {
+ co_await timer();
+ throw std::runtime_error("Invalid result");
+ co_return 42;
+ };
+
+ try {
+ const auto result = co_await coro();
+ QCORO_FAIL("Exception was not propagated.");
+ Q_UNUSED(result);
+ } catch (const std::runtime_error &) {
+ // OK
+ } catch (...) {
+ QCORO_FAIL("Exception type was not propagated, or other exception was thrown.");
+ }
+ }
+
+ QCoro::Task<> testVoidCoroutineWithException_coro(QCoro::TestContext) {
+ const auto coro = []() -> QCoro::Task<> {
+ co_await timer();
+ throw std::runtime_error("Error");
+ };
+
+ try {
+ co_await coro();
+ QCORO_FAIL("Exception was not propagated.");
+ } catch (const std::runtime_error &) {
+ // OK
+ } catch (...) {
+ QCORO_FAIL("Exception type was not propagated, or other exception was thrown.");
+ }
+ }
+
+ QCoro::Task<> testCoroutineFrameDestroyed_coro(QCoro::TestContext) {
+ bool destroyed = false;
+ const auto coro = [&destroyed]() -> QCoro::Task<> {
+ const auto guard = qScopeGuard([&destroyed]() mutable {
+ destroyed = true;
+ });
+
+ QCORO_VERIFY(!destroyed);
+ co_await timer();
+ QCORO_VERIFY(!destroyed);
+ };
+
+ co_await coro();
+ QCORO_VERIFY(destroyed);
+ }
+
+ QCoro::Task<> testExceptionPropagation_coro(QCoro::TestContext) {
+ QCORO_VERIFY_EXCEPTION_THROWN(
+ co_await []() -> QCoro::Task<int> {
+ throw std::runtime_error("Test!");
+ co_return 42;
+ }(),
+ std::runtime_error);
+
+ QCORO_VERIFY_EXCEPTION_THROWN(
+ co_await []() -> QCoro::Task<> {
+ throw std::runtime_error("Test!");
+ }(),
+ std::runtime_error);
+
+ QCORO_VERIFY_EXCEPTION_THROWN(
+ co_await []() -> QCoro::Task<int> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ co_return 42;
+ }(),
+ std::runtime_error);
+
+ QCORO_VERIFY_EXCEPTION_THROWN(
+ co_await []() -> QCoro::Task<> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ }(),
+ std::runtime_error);
+ }
+
+ QCoro::Task<> testThenReturnValueNoArgument_coro(QCoro::TestContext) {
+ auto task = timer().then([]() {
+ return 42;
+ });
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<int>>);
+ const auto result = co_await task;
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testThenReturnValueWithArgument_coro(QCoro::TestContext) {
+ auto task = timerWithValue(42).then([](int param) -> QCoro::Task<int> {
+ co_return param * 2;
+ });
+
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<int>>);
+ const auto result = co_await task;
+ QCORO_COMPARE(result, 84);
+ }
+
+ QCoro::Task<> testThenReturnTaskVoidNoArgument_coro(QCoro::TestContext) {
+ auto task = timer().then([]() -> QCoro::Task<void> {
+ co_await timer();
+ });
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<void>>);
+ co_await task;
+ }
+
+ QCoro::Task<> testThenReturnTaskVoidWithArgument_coro(QCoro::TestContext) {
+ auto task = timerWithValue(42).then([](int result) -> QCoro::Task<void> {
+ co_await timer();
+ Q_UNUSED(result);
+ });
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<void>>);
+ co_await task;
+ }
+
+ QCoro::Task<> testThenReturnTaskTNoArgument_coro(QCoro::TestContext) {
+ auto task = timer().then([]() -> QCoro::Task<int> {
+ co_await timer();
+ co_return 42;
+ });
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<int>>);
+ const auto result = co_await task;
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testThenReturnTaskTWithArgument_coro(QCoro::TestContext) {
+ auto task = timerWithValue(42).then([](int val) -> QCoro::Task<int> {
+ co_await timer();
+ co_return val * 2;
+ });
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<int>>);
+ const auto result = co_await task;
+ QCORO_COMPARE(result, 84);
+ }
+
+ QCoro::Task<> testThenReturnValueSync_coro(QCoro::TestContext context) {
+ context.setShouldNotSuspend();
+
+ auto task = []() -> QCoro::Task<int> {
+ co_return 42;
+ }().then([](int param) {
+ return param * 2;
+ });
+ const int result = co_await task;
+ QCORO_COMPARE(result, 84);
+ }
+
+ QCoro::Task<> testThenScopeAwait_coro(QCoro::TestContext) {
+ const int result = co_await thenScopeTestFuncWithValue(42);
+ QCORO_COMPARE(result, 42);
+ }
+
+ QCoro::Task<> testThenExceptionPropagation_coro(QCoro::TestContext) {
+ QCORO_VERIFY_EXCEPTION_THROWN(
+ co_await []() -> QCoro::Task<int> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ co_return 42;
+ }().then([](int) -> QCoro::Task<> {
+ QCORO_FAIL("The then() callback should never be called");
+ co_return;
+ }),
+ std::runtime_error);
+ }
+
+ QCoro::Task<> testThenError_coro(QCoro::TestContext) {
+ bool exceptionThrown = false;
+
+ co_await []() -> QCoro::Task<int> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ co_return 42;
+ }().then([](int) -> QCoro::Task<> {
+ QCORO_FAIL("The then() callback should not be called");
+ },
+ [&exceptionThrown](const std::exception &) {
+ exceptionThrown = true;
+ }
+ );
+
+ QCORO_VERIFY(exceptionThrown);
+ }
+
+ QCoro::Task<> testThenErrorWithValue_coro(QCoro::TestContext) {
+ bool exceptionThrown = false;
+ bool thenCalled = false;
+
+ const int result = co_await []() -> QCoro::Task<> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ }().then([&thenCalled]() -> QCoro::Task<int> {
+ thenCalled = true;
+ co_return 42;
+ },
+ [&exceptionThrown](const std::exception &) {
+ exceptionThrown = true;
+ }
+ );
+
+ // We handled an exception, so there's no error and it should
+ // be default-constructed.
+ QCORO_COMPARE(result, 0);
+ QCORO_VERIFY(!thenCalled);
+ QCORO_VERIFY(exceptionThrown);
+ }
+
+ void testThenImplicitArgumentConversion_coro(TestLoop &el) {
+ QTimer test;
+ QString result;
+ qCoro(test).waitForTimeout().then([]() -> QCoro::Task<ImplicitConversionBar> {
+ ImplicitConversionBar bar{42};
+ co_await timer(10ms);
+ co_return bar;
+ }).then([&](ImplicitConversionFoo foo) {
+ result = foo.string;
+ el.quit();
+ });
+ test.start(10ms);
+ el.exec();
+
+ QCOMPARE(result, QStringLiteral("42"));
+ }
+
+ void testReturnValueImplicitConversion(QCoro::TestContext) {
+ const auto testcoro [[maybe_unused]] = []() -> QCoro::Task<int> {
+ co_return 42LL;
+ };
+ }
+
+ QCoro::Task<> testMultipleAwaiters_coro(QCoro::TestContext) {
+ auto task = timer(100ms);
+
+ bool called = false;
+ // Internally co_awaits task
+ task.then([&called]() {
+ called = true;
+ });
+
+ co_await task;
+
+ QCORO_VERIFY(called);
+ }
+
+ QCoro::Task<> testMultipleAwaitersSync_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ auto task = []() -> QCoro::Task<> { co_return; }();
+
+ bool called = false;
+ task.then([&called]() {
+ called = true;
+ });
+
+ co_await task;
+
+ QCORO_VERIFY(called);
+ }
+
+ Q_SIGNAL void callbackCalled();
+
+ template <typename QObjectDerived, typename Signal>
+ QCoro::Task<> verifySignalEmitted(QObjectDerived *context, Signal &&signal) {
+ bool called = false;
+ co_await qCoro(context, std::move(signal)).then([&]() {
+ called = true;
+ });
+ QCORO_VERIFY(called);
+ }
+
+ QCoro::Task<> testTaskConnect_coro(QCoro::TestContext) {
+ // Test that free functions can be passed as callback
+ QCoro::connect(timer(), this, [this]() {
+ Q_EMIT callbackCalled();
+ });
+ co_await verifySignalEmitted(this, &QCoroTaskTest::callbackCalled);
+
+ // Check that member functions can be passed as callback
+ QCoro::connect(timer(), this, &QCoroTaskTest::callbackCalled);
+ co_await verifySignalEmitted(this, &QCoroTaskTest::callbackCalled);
+
+ // Test that the code still compiles if the value of the coroutine is not used by the function.
+ auto nonVoidCoroutine = []() -> QCoro::Task<QString> {
+ co_await timer();
+ co_return QStringLiteral("Hello World!");
+ };
+ QCoro::connect(nonVoidCoroutine(), this, [this]() {
+ Q_EMIT callbackCalled();
+ });
+ co_await verifySignalEmitted(this, &QCoroTaskTest::callbackCalled);
+
+ QCoro::connect(nonVoidCoroutine(), this, [this](QString) {
+ Q_EMIT callbackCalled();
+ });
+ co_await verifySignalEmitted(this, &QCoroTaskTest::callbackCalled);
+ }
+
+private Q_SLOTS:
+ addTest(SimpleCoroutine)
+ addTest(CoroutineValue)
+ addTest(CoroutineMoveValue)
+ addTest(SyncCoroutine)
+ addTest(CoroutineWithException)
+ addTest(VoidCoroutineWithException)
+ addTest(CoroutineFrameDestroyed)
+ addTest(ExceptionPropagation)
+ addTest(ThenReturnValueNoArgument)
+ addTest(ThenReturnValueWithArgument)
+ addTest(ThenReturnTaskVoidNoArgument)
+ addTest(ThenReturnTaskVoidWithArgument)
+ addTest(ThenReturnTaskTNoArgument)
+ addTest(ThenReturnTaskTWithArgument)
+ addTest(ThenReturnValueSync)
+ addTest(ThenScopeAwait)
+ addTest(ThenExceptionPropagation)
+ addTest(ThenError)
+ addTest(ThenErrorWithValue)
+ addTest(TaskConnect)
+ addThenTest(ImplicitArgumentConversion)
+ addTest(MultipleAwaiters)
+ addTest(MultipleAwaitersSync)
+
+ // See https://github.com/danvratil/qcoro/issues/24
+ void testEarlyReturn()
+ {
+ QEventLoop loop;
+
+ const auto testReturn = [](bool immediate) -> QCoro::Task<bool> {
+ if (immediate) {
+ co_return true;
+ } else {
+ co_await timer();
+ co_return true;
+ }
+ };
+
+ bool immediateResult = false;
+ bool delayedResult = false;
+
+ const auto testImmediate = [&]() -> QCoro::Task<> {
+ immediateResult = co_await testReturn(true);
+ };
+
+ const auto testDelayed = [&]() -> QCoro::Task<> {
+ delayedResult = co_await testReturn(false);
+ loop.quit();
+ };
+
+ QMetaObject::invokeMethod(
+ &loop, [&]() { testImmediate(); }, Qt::QueuedConnection);
+ QMetaObject::invokeMethod(
+ &loop, [&]() { testDelayed(); }, Qt::QueuedConnection);
+
+ loop.exec();
+
+ QVERIFY(immediateResult);
+ QVERIFY(delayedResult);
+ }
+
+ // TODO: Test timeout
+ void testWaitFor() {
+ QCoro::waitFor(timer());
+ }
+
+ // TODO: Test timeout
+ void testWaitForWithValue() {
+ const auto result = QCoro::waitFor([]() -> QCoro::Task<int> {
+ co_await timer();
+ co_return 42;
+ }());
+ QCOMPARE(result, 42);
+ }
+
+ void testEarlyReturnWaitFor() {
+ QCoro::waitFor([]() -> QCoro::Task<> { co_return; }());
+ }
+
+ void testEarlyReturnWaitForWithValue() {
+ const auto result = QCoro::waitFor([]() -> QCoro::Task<int> {
+ co_return 42;
+ }());
+ QCOMPARE(result, 42);
+ }
+
+ void testWaitForAwaitable() {
+ TestAwaitable<int> awaitable(42);
+ QElapsedTimer timer;
+ timer.start();
+
+ static_assert(std::is_same_v<decltype(QCoro::waitFor(awaitable)), int>);
+ const int result = QCoro::waitFor(awaitable);
+ QCOMPARE(result, 42);
+ QVERIFY(timer.elapsed() >= static_cast<float>(awaitable.delay().count()) * 0.9);
+
+ }
+
+ void testWaitForVoidAwaitable() {
+ TestAwaitable<void> awaitable;
+ QElapsedTimer timer;
+ timer.start();
+
+ static_assert(std::is_void_v<decltype(QCoro::waitFor(awaitable))>);
+ QCoro::waitFor(awaitable);
+
+ QVERIFY(timer.elapsed() >= static_cast<float>(awaitable.delay().count()) * 0.9);
+ }
+
+ void testWaitForAwaitableWithOperatorCoAwait() {
+ TestAwaitableWithCoAwait<int> awaitable(42);
+ QCoro::waitFor(awaitable);
+ QElapsedTimer timer;
+ timer.start();
+
+ static_assert(std::is_same_v<decltype(QCoro::waitFor(awaitable)), int>);
+ const int result = QCoro::waitFor(awaitable);
+ QCOMPARE(result, 42);
+ QVERIFY(timer.elapsed() >= (90ms).count());
+ }
+
+ void testWaitForVoidAwaitableWithOperatorCoAwait() {
+ TestAwaitableWithCoAwait<void> awaitable;
+ QElapsedTimer timer;
+ timer.start();
+
+ static_assert(std::is_void_v<decltype(QCoro::waitFor(awaitable))>);
+ QCoro::waitFor(awaitable);
+
+ QVERIFY(timer.elapsed() >= (90ms).count());
+ }
+
+ void testWaitForWithValueRethrowsException() {
+ const auto coro = []() -> QCoro::Task<int> {
+ co_await timer();
+ throw std::runtime_error("Exception");
+ co_return 42;
+ };
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
+ QVERIFY_THROWS_EXCEPTION(std::runtime_error, QCoro::waitFor(coro()));
+#else
+ QVERIFY_EXCEPTION_THROWN(QCoro::waitFor(coro()), std::runtime_error);
+#endif
+ }
+
+ void testWaitForRethrowsException() {
+ const auto coro = []() -> QCoro::Task<> {
+ co_await timer();
+ throw std::runtime_error("Exception");
+ };
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
+ QVERIFY_THROWS_EXCEPTION(std::runtime_error, QCoro::waitFor(coro()));
+#else
+ QVERIFY_EXCEPTION_THROWN(QCoro::waitFor(coro()), std::runtime_error);
+#endif
+ }
+
+
+ void testIgnoredVoidTaskResult() {
+ QEventLoop el;
+ ignoreCoroutineResult(el, [&el]() -> QCoro::Task<> {
+ co_await timer();
+ el.quit();
+ });
+ }
+
+ void testIgnoredValueTaskResult() {
+ QEventLoop el;
+ ignoreCoroutineResult(el, [&el]() -> QCoro::Task<QString> {
+ co_await timer();
+ el.quit();
+ co_return QStringLiteral("Result");
+ });
+ }
+
+ void testThenVoidNoArgument() {
+ QEventLoop el;
+
+ {
+ timer().then([&el]() {
+ el.quit();
+ });
+ }
+
+ el.exec();
+ }
+
+ void testThenDiscardsReturnValue() {
+ QEventLoop el;
+ bool called = false;
+
+ timerWithValue(42).then([&]() {
+ el.quit();
+ called = true;
+ });
+
+ el.exec();
+
+ QVERIFY(called);
+ }
+
+ void testThenScope() {
+ QEventLoop el;
+ thenScopeTestFunc(&el);
+ el.exec();
+ }
+
+ void testThenVoidWithArgument() {
+ QEventLoop el;
+ int result = 0;
+
+ {
+ timerWithValue(42).then([&el, &result](int val) {
+ result = val;
+ el.quit();
+ });
+ }
+
+ el.exec();
+ QCOMPARE(result, 42);
+ }
+
+ void testThenVoidWithFunction() {
+ QEventLoop el;
+
+ timerWithValue(10ms).then(timer).then([&el]() {
+ el.quit();
+ });
+
+ el.exec();
+ }
+
+ void testThenErrorInCallback() {
+ QEventLoop el;
+ QTimer::singleShot(5s, &el, [&el]() {
+ el.quit();
+ QFAIL("Timeout waiting for coroutine");
+ });
+
+ []() -> QCoro::Task<> {
+ co_await timer();
+ }().then([]() {
+ throw std::runtime_error("Test!");
+ }, [](const std::exception &) {
+ QFAIL("Continuation exception should not be handled by the same error handled");
+ }).then([]() {
+ QFAIL("Second then continuation should not be called.");
+ }, [&el](const std::exception &) {
+ el.quit();
+ });
+
+ el.exec();
+ }
+
+ void testThenExceptionInError() {
+ QEventLoop el;
+ QTimer::singleShot(5s, &el, [&el]() {
+ el.quit();
+ QFAIL("Timeout waiting for coroutine");
+ });
+
+ []() -> QCoro::Task<> {
+ co_await timer();
+ throw std::runtime_error("Test!");
+ }().then([]() {
+ QFAIL("The then() continuation should not be called");
+ }, [](const std::exception &) {
+ throw std::runtime_error("Another test!");
+ }).then([]() {
+ QFAIL("Second then() continuation should not be called");
+ }, [&el](const std::exception &) {
+ el.quit();
+ });
+
+ el.exec();
+ }
+
+ void testTaskConnectContext_coro() {
+ auto task = timer(200ms);
+ static_assert(std::is_same_v<decltype(task), QCoro::Task<>>);
+
+ bool called = false;
+ auto context = new QObject();
+
+ QCoro::connect(task, context, [&]() {
+ called = true;
+ });
+
+ // Delete context, callback should not be called
+ delete context;
+
+ QCoro::waitFor(task);
+
+ QVERIFY(!called);
+ }
+
+
+};
+
+QTEST_GUILESS_MAIN(QCoroTaskTest)
+
+#include "qcorotask.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcoro/network/qcorotcpserver.h"
+#include "qcoro/network/qcoroabstractsocket.h"
+
+#include <QTcpServer>
+#include <QTcpSocket>
+
+#include <thread>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+class Client {
+public:
+ Client(uint16_t serverPort, std::mutex &mutex, bool &ok)
+ : mThread([serverPort, &mutex, &ok]() mutable {
+ std::this_thread::sleep_for(500ms);
+
+ std::lock_guard lock{mutex};
+ QTcpSocket socket;
+ socket.connectToHost(QHostAddress::LocalHost, serverPort);
+ if (!socket.waitForConnected(10'000)) {
+ qWarning() << "Not connected within timeout" << socket.errorString();
+ ok = false;
+ return;
+ }
+ socket.write("Hello World!");
+ socket.flush();
+ socket.close();
+ ok = true;
+ })
+ {}
+
+ ~Client() {
+ mThread.join();
+ }
+
+private:
+ std::thread mThread;
+};
+
+class QCoroTcpServerTest: public QCoro::TestObject<QCoroTcpServerTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testWaitForNewConnectionTriggers_coro(QCoro::TestContext) {
+ QTcpServer server;
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+ QCORO_VERIFY(server.isListening());
+ const quint16 serverPort = server.serverPort();
+
+ std::mutex mutex;
+ bool ok = false;
+ Client client(serverPort, mutex, ok);
+
+ auto *connection = co_await qCoro(server).waitForNewConnection(10s);
+ QCORO_VERIFY(connection != nullptr);
+ const auto data = co_await qCoro(connection).readAll();
+ QCORO_COMPARE(data, QByteArray{"Hello World!"});
+
+ std::lock_guard lock{mutex};
+ QCORO_VERIFY(ok);
+ }
+
+ void testThenWaitForNewConnectionTriggers_coro(TestLoop &el) {
+ QTcpServer server;
+ QVERIFY(server.listen(QHostAddress::LocalHost));
+ const quint16 serverPort = server.serverPort();
+
+ std::mutex mutex;
+ bool ok = false;
+ Client client(serverPort, mutex, ok);
+
+ bool called = false;
+ qCoro(server).waitForNewConnection(10s).then([&](QTcpSocket *socket) -> QCoro::Task<void> {
+ called = true;
+ if (!socket) {
+ el.quit();
+ co_return;
+ }
+
+ const auto data = co_await qCoro(socket).readAll();
+ QCORO_COMPARE(data, QByteArray("Hello World!"));
+ el.quit();
+ });
+ el.exec();
+
+ std::lock_guard lock{mutex};
+ QVERIFY(called);
+ QVERIFY(ok);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitPendingConnection_coro(QCoro::TestContext testContext) {
+ testContext.setShouldNotSuspend();
+
+ QTcpServer server;
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+ const int serverPort = server.serverPort();
+
+ bool ok = false;
+ std::mutex mutex;
+ Client client(serverPort, mutex, ok);
+
+ QCORO_VERIFY(server.waitForNewConnection(10'000));
+
+ auto *connection = co_await qCoro(server).waitForNewConnection(10s);
+
+ connection->waitForReadyRead(); // can't use coroutine, it might suspend or not, depending on how eventloop
+ // gets triggered, which fails the test since it's setShouldNotSuspend()
+ QCORO_COMPARE(connection->readAll(), QByteArray{"Hello World!"});
+
+ std::lock_guard lock{mutex};
+ QCORO_VERIFY(ok);
+ }
+
+private Q_SLOTS:
+ addCoroAndThenTests(WaitForNewConnectionTriggers)
+ addTest(DoesntCoAwaitPendingConnection)
+};
+
+QTEST_GUILESS_MAIN(QCoroTcpServerTest)
+
+#include "qcorotcpserver.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcoro/core/qcorothread.h"
+#include "qcoro/core/qcorosignal.h"
+
+#include <QThread>
+#include <QScopeGuard>
+
+#include <thread>
+#include <memory>
+
+using namespace std::chrono_literals;
+
+class QCoroThreadTest : public QCoro::TestObject<QCoroThreadTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testWaitForStarted_coro(QCoro::TestContext) {
+ std::unique_ptr<QThread> thread(QThread::create([]() {
+ std::this_thread::sleep_for(100ms);
+ }));
+
+ const auto threadGuard = qScopeGuard([&]() { thread->wait(); });
+
+ QCORO_DELAY(thread->start());
+
+ const bool ok = co_await qCoro(thread.get()).waitForStarted();
+ QCORO_VERIFY(thread->isRunning());
+ QCORO_VERIFY(ok);
+ }
+
+ QCoro::Task<> testWaitForFinished_coro(QCoro::TestContext) {
+ std::unique_ptr<QThread> thread(QThread::create([]() {
+ std::this_thread::sleep_for(100ms);
+ }));
+
+ thread->start();
+ co_await qCoro(thread.get()).waitForStarted();
+ QCORO_VERIFY(thread->isRunning());
+ const bool ok = co_await qCoro(thread.get()).waitForFinished();
+ QCORO_VERIFY(thread->isFinished());
+
+ QCORO_VERIFY(ok);
+ }
+
+ QCoro::Task<> testMoveToThread_coro(QCoro::TestContext) {
+ QThread newThread;
+ newThread.start();
+
+ QCORO_COMPARE(QThread::currentThread(), QCoreApplication::instance()->thread());
+
+ co_await QCoro::moveToThread(&newThread);
+
+ QCORO_COMPARE(QThread::currentThread(), &newThread);
+
+ co_await QCoro::moveToThread(qApp->thread());
+
+ QCORO_COMPARE(QThread::currentThread(), QCoreApplication::instance()->thread());
+
+ newThread.exit();
+ newThread.wait();
+ }
+
+private Q_SLOTS:
+ addTest(WaitForStarted)
+ addTest(WaitForFinished)
+ addTest(MoveToThread)
+};
+
+
+QTEST_GUILESS_MAIN(QCoroThreadTest)
+
+#include "qcorothread.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Joey Richey <joey@dogsplayingpoker.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcorotimer.h"
+
+#include <chrono>
+
+class QCoroWaitForTest : public QCoro::TestObject<QCoroWaitForTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<void> testPrimitiveType_coro(QCoro::TestContext ctx)
+ {
+ ctx.setShouldNotSuspend();
+ auto task_test = []() -> QCoro::Task<int> {
+ co_return 7;
+ };
+
+ const int ret = QCoro::waitFor(task_test());
+ QCORO_VERIFY(ret == 7);
+ }
+
+ QCoro::Task<void> testDefaultConstructible_coro(QCoro::TestContext ctx)
+ {
+ ctx.setShouldNotSuspend();
+ auto task_test = []() -> QCoro::Task<std::string> {
+ co_return "seven";
+ };
+
+ const std::string ret = QCoro::waitFor(task_test());
+ QCORO_VERIFY(ret == "seven");
+ }
+
+ QCoro::Task<void> testNonDefaultConstructible_coro(QCoro::TestContext ctx)
+ {
+ ctx.setShouldNotSuspend();
+
+ struct test_struct {
+ explicit test_struct(int i_) : i(i_) {}
+ int i;
+ };
+
+ auto task_test = []() -> QCoro::Task<test_struct> {
+ co_return test_struct(7);
+ };
+
+ const test_struct ret = QCoro::waitFor(task_test());
+ QCORO_VERIFY(ret.i == 7);
+ }
+
+private Q_SLOTS:
+ addTest(PrimitiveType)
+ addTest(DefaultConstructible)
+ addTest(NonDefaultConstructible)
+};
+
+QTEST_GUILESS_MAIN(QCoroWaitForTest)
+
+#include "qcorowaitfor.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+#include "testwsserver.h"
+#include "qcoro/websockets/qcorowebsocket.h"
+
+#include <QWebSocket>
+
+class QCoroWebSocketTest : public QCoro::TestObject<QCoroWebSocketTest> {
+ Q_OBJECT
+public:
+ explicit QCoroWebSocketTest(QObject *parent = nullptr)
+ : QCoro::TestObject<QCoroWebSocketTest>(parent)
+ {
+ // On Windows, constructing QWebSocket for the first time takes some time
+ // (most likely due to loading OpenSSL), which causes the first test to
+ // time out on the CI.
+ QWebSocket socket;
+ }
+private:
+ template<typename T, typename SendFunc, typename RecvFunc>
+ QCoro::Task<> testReceived(const T &msg, SendFunc sendFunc, RecvFunc recvFunc) {
+ QWebSocket socket;
+ QCORO_VERIFY(connectSocket(socket));
+
+ QCORO_DELAY(std::invoke(sendFunc, socket, msg));
+ auto coroSocket = qCoro(socket);
+ auto gen = std::invoke(recvFunc, &coroSocket, std::chrono::milliseconds{-1});
+ const auto data = co_await gen.begin();
+ QCORO_VERIFY(data != gen.end());
+ if constexpr (std::is_same_v<std::remove_cvref_t<decltype(*data)>, QString> ||
+ std::is_same_v<std::remove_cvref_t<decltype(*data)>, QByteArray>) {
+ QCORO_COMPARE(*data, msg);
+ } else {
+ QCORO_COMPARE(std::get<0>(*data), msg);
+ QCORO_COMPARE(std::get<1>(*data), true);
+ }
+ }
+
+ template<typename RecvFunc>
+ QCoro::Task<> testTimeout(RecvFunc recvFunc) {
+ mServer.setExpectTimeout();
+
+ QWebSocket socket;
+ QCORO_VERIFY(connectSocket(socket));
+
+ auto coroSocket = qCoro(socket);
+ auto gen = std::invoke(recvFunc, &coroSocket, 10ms);
+ const auto data = co_await gen.begin();
+ QCORO_COMPARE(data, gen.end());
+ }
+
+ template<typename RecvFunc>
+ QCoro::Task<> testGeneratorEndOnSocketClose(RecvFunc recvFunc) {
+ mServer.setExpectTimeout();
+
+ QWebSocket socket;
+ QCORO_VERIFY(connectSocket(socket));
+
+ QCORO_DELAY(socket.close());
+ auto coroSocket = qCoro(socket);
+ auto gen = std::invoke(recvFunc, &coroSocket, std::chrono::milliseconds{-1});
+ const auto it = co_await gen.begin();
+ QCORO_COMPARE(it, gen.end());
+ }
+
+private:
+ QCoro::Task<> testWaitForOpenWithUrl_coro(QCoro::TestContext) {
+ QWebSocket socket;
+ const auto result = co_await qCoro(socket).open(mServer.url());
+ QCORO_VERIFY(result);
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForOpenWithUrl_coro(TestLoop &el) {
+ QWebSocket socket;
+ bool called = false;
+ qCoro(socket).open(mServer.url()).then([&el, &called](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testTimeoutOpenWithUrl_coro(QCoro::TestContext) {
+ QWebSocket socket;
+ const auto url = mServer.url();
+ mServer.stop(); // stop the server so we cannot connect
+
+ const auto result = co_await qCoro(socket).open(url, 10ms);
+ QCORO_VERIFY(!result);
+ }
+
+ void testThenTimeoutOpenWithUrl_coro(TestLoop &el) {
+ QWebSocket socket;
+ const auto url = mServer.url();
+ mServer.stop();
+ bool called = false;
+ qCoro(socket).open(url, 10ms).then([&el, &called](bool connected) {
+ el.quit();
+ called = true;
+ QVERIFY(!connected);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testWaitForOpenWithNetworkRequest_coro(QCoro::TestContext) {
+ QWebSocket socket;
+ QNetworkRequest request(mServer.url());
+ const auto result = co_await qCoro(socket).open(request);
+ QCORO_VERIFY(result);
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ QCORO_VERIFY(mServer.waitForConnection());
+ }
+
+ void testThenWaitForOpenWithNetworkRequest_coro(TestLoop &el) {
+ QWebSocket socket;
+ bool called = false;
+ qCoro(socket).open(QNetworkRequest{mServer.url()}).then([&el, &called](bool connected) {
+ called = true;
+ el.quit();
+ QVERIFY(connected);
+ });
+ el.exec();
+ QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ QVERIFY(called);
+ QVERIFY(mServer.waitForConnection());
+ }
+
+ QCoro::Task<> testDoesntCoawaitOpenedSocket_coro(QCoro::TestContext ctx) {
+ QWebSocket socket;
+ QCORO_VERIFY(connectSocket(socket));
+
+ ctx.setShouldNotSuspend();
+
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+ const auto connected = co_await qCoro(socket).open(mServer.url());
+ QCORO_VERIFY(connected);
+ }
+
+ QCoro::Task<> testPing_coro(QCoro::TestContext) {
+ QWebSocket socket;
+ QCORO_VERIFY(connectSocket(socket));
+
+ const auto response = co_await qCoro(socket).ping("PING!");
+ QCORO_VERIFY(response.has_value());
+ QCORO_VERIFY(*response >= 0ms); // the latency will be somewhere around 0
+ }
+
+ void testThenPing_coro(TestLoop &el) {
+ QWebSocket socket;
+ QVERIFY(connectSocket(socket));
+ bool called = false;
+ qCoro(socket).ping("PING!").then([&el, &called](std::optional<std::chrono::milliseconds> pong) {
+ el.quit();
+ called = true;
+ QVERIFY(pong.has_value());
+ QVERIFY(*pong >= 0ms);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testBinaryFrame_coro(QCoro::TestContext) {
+ co_await testReceived(QByteArray("TEST MESSAGE"), &QWebSocket::sendBinaryMessage,
+ &QCoro::detail::QCoroWebSocket::binaryFrames);
+ }
+
+ QCoro::Task<> testBinaryFrameTimeout_coro(QCoro::TestContext) {
+ co_await testTimeout(&QCoro::detail::QCoroWebSocket::binaryFrames);
+ }
+
+ QCoro::Task<> testBinaryFrameGeneratorEndsOnSocketClose_coro(QCoro::TestContext) {
+ co_await testGeneratorEndOnSocketClose(&QCoro::detail::QCoroWebSocket::binaryFrames);
+ }
+
+ QCoro::Task<> testBinaryMessage_coro(QCoro::TestContext) {
+ co_await testReceived(QByteArray("TEST MESSAGE"), &QWebSocket::sendBinaryMessage,
+ &QCoro::detail::QCoroWebSocket::binaryMessages);
+ }
+
+ QCoro::Task<> testBinaryMessageTimeout_coro(QCoro::TestContext) {
+ co_await testTimeout(&QCoro::detail::QCoroWebSocket::binaryMessages);
+ }
+
+ QCoro::Task<> testBinaryMessageGeneratorEndsOnSocketClose_coro(QCoro::TestContext) {
+ co_await testGeneratorEndOnSocketClose(&QCoro::detail::QCoroWebSocket::binaryMessages);
+ }
+
+ QCoro::Task<> testTextFrame_coro(QCoro::TestContext) {
+ co_await testReceived(QStringLiteral("TEST MESSAGE"), &QWebSocket::sendTextMessage,
+ &QCoro::detail::QCoroWebSocket::textFrames);
+ }
+
+ QCoro::Task<> testTextFrameTimeout_coro(QCoro::TestContext) {
+ co_await testTimeout(&QCoro::detail::QCoroWebSocket::textFrames);
+ }
+
+ QCoro::Task<> testTextFrameGeneratorEndsOnSocketClose_coro(QCoro::TestContext) {
+ co_await testGeneratorEndOnSocketClose(&QCoro::detail::QCoroWebSocket::textFrames);
+ }
+
+ QCoro::Task<> testTextMessage_coro(QCoro::TestContext) {
+ co_await testReceived(QStringLiteral("TEST MESSAGE"), &QWebSocket::sendTextMessage,
+ &QCoro::detail::QCoroWebSocket::textMessages);
+ }
+
+ QCoro::Task<> testTextMessageTimeout_coro(QCoro::TestContext) {
+ co_await testTimeout(&QCoro::detail::QCoroWebSocket::textMessages);
+ }
+
+ QCoro::Task<> testTextMessageGeneratorEndsOnSocketClose_coro(QCoro::TestContext) {
+ co_await testGeneratorEndOnSocketClose(&QCoro::detail::QCoroWebSocket::textMessages);
+ }
+
+ QCoro::Task<> testReadFragmentedMessage_coro(QCoro::TestContext) {
+ QWebSocket socket;
+ QUrl url = mServer.url();
+ url.setPath(QStringLiteral("/large"));
+ QCORO_VERIFY(QCoro::waitFor(qCoro(socket).open(url)));
+
+ QCORO_DELAY(socket.sendBinaryMessage("One large, please"));
+
+ auto frames = qCoro(socket).binaryFrames();
+ QByteArray data;
+ for (auto frame = co_await frames.begin(), end = frames.end(); frame != end; co_await ++frame) {
+ data += std::get<0>(*frame);
+ if (std::get<1>(*frame)) { // last
+ break;
+ }
+ }
+
+ QCORO_VERIFY(data.size() >= 10 * 1024 * 1024); // 10MB
+ }
+
+private Q_SLOTS:
+ void init() {
+ mServer.start();
+ }
+
+ void cleanup() {
+ mServer.stop();
+ }
+
+ addCoroAndThenTests(WaitForOpenWithUrl)
+ addCoroAndThenTests(TimeoutOpenWithUrl)
+ addCoroAndThenTests(WaitForOpenWithNetworkRequest)
+ addTest(DoesntCoawaitOpenedSocket)
+ addCoroAndThenTests(Ping)
+ addTest(BinaryFrame)
+ addTest(BinaryFrameTimeout)
+ addTest(BinaryFrameGeneratorEndsOnSocketClose)
+ addTest(BinaryMessage)
+ addTest(BinaryMessageTimeout)
+ addTest(BinaryMessageGeneratorEndsOnSocketClose)
+ addTest(TextFrame)
+ addTest(TextFrameTimeout)
+ addTest(TextFrameGeneratorEndsOnSocketClose)
+ addTest(TextMessage)
+ addTest(TextMessageTimeout)
+ addTest(TextMessageGeneratorEndsOnSocketClose)
+
+ addTest(ReadFragmentedMessage)
+
+private:
+ bool connectSocket(QWebSocket &socket) {
+ return QCoro::waitFor(qCoro(socket).open(mServer.url()));
+ }
+
+ TestWsServer mServer;
+};
+
+QTEST_GUILESS_MAIN(QCoroWebSocketTest)
+
+#include "qcorowebsocket.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "websockets/qcorowebsocketserver.h"
+#include "websockets/qcorowebsocket.h"
+#include "testobject.h"
+
+#include <QWebSocketServer>
+#include <QWebSocket>
+
+#include <QTest>
+
+class QCoroWebSocketServerTest : public QCoro::TestObject<QCoroWebSocketServerTest> {
+ Q_OBJECT
+public:
+ QCoroWebSocketServerTest(QObject *parent = nullptr)
+ : QCoro::TestObject<QCoroWebSocketServerTest>(parent)
+ {
+ // On Windows, constructing QWebSocket for the first time takes some time
+ // (most likely due to loading OpenSSL), which causes the first test to
+ // time out on the CI.
+ QWebSocket socket;
+ }
+
+private:
+ QCoro::Task<> testNextPendingConnection_coro(QCoro::TestContext) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+
+ QWebSocket socket;
+ QCORO_DELAY(socket.open(server.serverUrl()));
+ const auto serverSocket = std::unique_ptr<QWebSocket>(co_await qCoro(server).nextPendingConnection());
+ QCORO_VERIFY(serverSocket != nullptr);
+ }
+
+ void testThenNextPendingConnection_coro(TestLoop &el) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QVERIFY(server.listen(QHostAddress::LocalHost));
+
+ QWebSocket socket;
+ QCORO_DELAY(socket.open(server.serverUrl()));
+
+ bool called = false;
+ qCoro(server).nextPendingConnection().then([&el, &called](QWebSocket *socket) {
+ el.quit();
+ called = true;
+ QVERIFY(socket != nullptr);
+ socket->deleteLater();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testNextPendingConnectionTimeout_coro(QCoro::TestContext) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+
+ const auto *socket = co_await qCoro(server).nextPendingConnection(10ms);
+ QCORO_COMPARE(socket, nullptr);
+ }
+
+ void testThenNextPendingConnectionTimeout_coro(TestLoop &el) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QVERIFY(server.listen(QHostAddress::LocalHost));
+
+ bool called = false;
+ qCoro(server).nextPendingConnection(100ms).then([&el, &called](QWebSocket *socket) {
+ el.quit();
+ called = true;
+ QCOMPARE(socket, nullptr);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testClosingServerResumesAwaiters_coro(QCoro::TestContext) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+ QCORO_DELAY(server.close());
+
+ auto *socket = co_await qCoro(server).nextPendingConnection();
+ QCORO_COMPARE(socket, nullptr);
+ }
+
+ void testThenClosingServerResumesAwaiters_coro(TestLoop &el) {
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QVERIFY(server.listen(QHostAddress::LocalHost));
+ QCORO_DELAY(server.close());
+
+ bool called = false;
+ qCoro(server).nextPendingConnection().then([&el, &called](QWebSocket *socket) {
+ el.quit();
+ called = true;
+ QCOMPARE(socket, nullptr);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testDoesntCoawaitNonlisteningServer_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+
+ const auto *socket = co_await qCoro(server).nextPendingConnection();
+ QCORO_COMPARE(socket, nullptr);
+ }
+
+ QCoro::Task<> testDoesntCoawaitWithPendingConnection_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ QWebSocketServer server(QStringLiteral("TestWSServer"), QWebSocketServer::NonSecureMode);
+ QCORO_VERIFY(server.listen(QHostAddress::LocalHost));
+
+ QWebSocket socket;
+ QCORO_VERIFY(QCoro::waitFor(qCoro(socket).open(server.serverUrl())));
+ QCORO_COMPARE(socket.state(), QAbstractSocket::ConnectedState);
+
+ QTest::qWait(100); // give the server time to register the incoming connection
+ QCORO_VERIFY(server.hasPendingConnections());
+
+ const auto serverSocket = std::unique_ptr<QWebSocket>(co_await qCoro(server).nextPendingConnection());
+ QCORO_VERIFY(serverSocket);
+ }
+
+private Q_SLOTS:
+ addCoroAndThenTests(NextPendingConnection)
+ addCoroAndThenTests(NextPendingConnectionTimeout)
+ addCoroAndThenTests(ClosingServerResumesAwaiters)
+ addTest(DoesntCoawaitNonlisteningServer)
+ addTest(DoesntCoawaitWithPendingConnection)
+};
+
+QTEST_GUILESS_MAIN(QCoroWebSocketServerTest)
+
+#include "qcorowebsocketserver.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testdbusserver.h"
+#include "testobject.h"
+
+#include "qcorodbuspendingcall.h"
+
+#include <QDBusConnection>
+#include <QDBusError>
+#include <QDBusInterface>
+#include <QDBusReply>
+
+class QCoroDBusPendingCallTest : public QCoro::TestObject<QCoroDBusPendingCallTest> {
+ Q_OBJECT
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ QCORO_VERIFY(iface.isValid());
+
+ const QDBusReply<void> reply = co_await iface.asyncCall(QStringLiteral("foo"));
+ QCORO_VERIFY(reply.isValid());
+ }
+
+ QCoro::Task<> testReturnsResult_coro(QCoro::TestContext) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ QCORO_VERIFY(iface.isValid());
+
+ const QDBusReply<QString> reply =
+ co_await iface.asyncCall(QStringLiteral("ping"), QStringLiteral("Hello there!"));
+
+ QCORO_VERIFY(reply.isValid());
+ QCORO_COMPARE(reply.value(), QStringLiteral("Hello there!"));
+ }
+
+ void testThenReturnsResult_coro(TestLoop &el) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ QVERIFY(iface.isValid());
+
+ const QDBusPendingCall call =
+ iface.asyncCall(QStringLiteral("ping"), QStringLiteral("Hello there!"));
+ bool called = false;
+ qCoro(call).waitForFinished().then([&](const QDBusMessage &msg) {
+ called = true;
+ el.quit();
+ QCOMPARE(QDBusReply<QString>(msg).value(), QStringLiteral("Hello there!"));
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testDoesntBlockEventLoop_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive;
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ QCORO_VERIFY(iface.isValid());
+
+ const QDBusReply<void> reply = co_await iface.asyncCall(QStringLiteral("blockFor"), 1);
+
+ QCORO_VERIFY(reply.isValid());
+ QCORO_VERIFY(eventLoopResponsive);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitFinishedCall_coro(QCoro::TestContext test) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ QCORO_VERIFY(iface.isValid());
+
+ auto call = iface.asyncCall(QStringLiteral("foo"));
+ QDBusReply<void> reply = co_await call;
+ QCORO_VERIFY(reply.isValid());
+
+ test.setShouldNotSuspend();
+
+ reply = co_await call;
+
+ QCORO_VERIFY(reply.isValid());
+ }
+
+private Q_SLOTS:
+ void initTestCase() {
+ for (int i = 0; i < 10; ++i) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ if (iface.isValid()) {
+ return;
+ }
+ QTest::qWait(100);
+ }
+
+ QFAIL("Failed to obtain a valid dbus interface");
+ }
+
+ void cleanupTestCase() {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ iface.call(QStringLiteral("quit"));
+ }
+
+ addTest(Triggers)
+ addCoroAndThenTests(ReturnsResult)
+ addTest(DoesntBlockEventLoop)
+ addTest(DoesntCoAwaitFinishedCall)
+};
+
+DBUS_TEST_MAIN(QCoroDBusPendingCallTest)
+
+#include "qdbuspendingcall.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "qcorodbustestinterface.h"
+#include "testdbusserver.h"
+#include "testobject.h"
+
+#include "qcorodbuspendingreply.h"
+
+#include <QDBusConnection>
+#include <QDBusError>
+#include <QDBusInterface>
+#include <QDBusReply>
+
+#include <memory>
+#include <thread>
+
+class QCoroDBusPendingCallTest : public QCoro::TestObject<QCoroDBusPendingCallTest> {
+ Q_OBJECT
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ const auto resp = co_await iface.foo();
+
+ QCORO_VERIFY(resp.isFinished());
+ }
+
+ QCoro::Task<> testQCoroWrapperTriggers_coro(QCoro::TestContext) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ const auto resp = co_await qCoro(iface.foo()).waitForFinished();
+
+ QCORO_VERIFY(resp.isFinished());
+ }
+
+ void testThenQCoroWrapperTriggers_coro(TestLoop &el) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QVERIFY(iface.isValid());
+
+ bool called = false;
+ qCoro(iface.foo()).waitForFinished().then([&](QDBusPendingReply<> reply) {
+ called = true;
+ el.quit();
+ QVERIFY(reply.isFinished());
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testReturnsResult_coro(QCoro::TestContext) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ const QString reply = co_await iface.ping(QStringLiteral("Hello there!"));
+
+ QCORO_COMPARE(reply, QStringLiteral("Hello there!"));
+ }
+
+ void testThenReturnsResult_coro(TestLoop &el) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QVERIFY(iface.isValid());
+
+ bool called = false;
+ qCoro(iface.ping(QStringLiteral("Hello there!"))).waitForFinished().then(
+ [&](const QDBusPendingReply<QString> &reply) {
+ called = true;
+ el.quit();
+ QCOMPARE(reply.value(), QStringLiteral("Hello there!"));
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testReturnsBlockingResult_coro(QCoro::TestContext) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ const QString reply = co_await iface.blockAndReturn(1);
+
+ QCORO_COMPARE(reply, QStringLiteral("Slept for 1 seconds"));
+ }
+
+ QCoro::Task<> testDoesntBlockEventLoop_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive;
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ const auto result = co_await iface.blockFor(1);
+
+ QCORO_VERIFY(result.isFinished());
+ QCORO_VERIFY(eventLoopResponsive);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitFinishedCall_coro(QCoro::TestContext test) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ auto call = iface.foo();
+ QDBusReply<void> reply = co_await call;
+ QCORO_VERIFY(reply.isValid());
+
+ test.setShouldNotSuspend();
+
+ reply = co_await call;
+
+ QCORO_VERIFY(reply.isValid());
+ }
+
+ void testThenDoesntCoAwaitFinishedCall_coro(TestLoop &el) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QVERIFY(iface.isValid());
+
+ auto call = iface.foo();
+ call.waitForFinished();
+
+ bool called = false;
+ qCoro(call).waitForFinished().then([&](QDBusPendingReply<>) {
+ called = true;
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testHandlesMultipleArguments_coro(QCoro::TestContext) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QCORO_VERIFY(iface.isValid());
+
+ QDBusPendingReply<QString, bool> reply = iface.asyncCall(QStringLiteral("blockAndReturnMultipleArguments"), 1);
+ co_await reply;
+
+ QCORO_VERIFY(reply.isFinished());
+ QCORO_COMPARE(reply.argumentAt<0>(), QStringLiteral("Hello World!"));
+ QCORO_COMPARE(reply.argumentAt<1>(), true);
+ }
+
+ void testThenHandlesMultipleArguments_coro(TestLoop &el) {
+ cz::dvratil::qcorodbustest iface(DBusServer::serviceName, DBusServer::objectPath,
+ QDBusConnection::sessionBus());
+ QVERIFY(iface.isValid());
+
+ QDBusPendingReply<QString, bool> reply = iface.asyncCall(QStringLiteral("blockAndReturnMultipleArguments"), 1);
+ bool called = false;
+ qCoro(reply).waitForFinished().then([&](const QDBusPendingReply<QString, bool> &reply) {
+ called = true;
+ el.quit();
+ QCOMPARE(reply.argumentAt<0>(), QStringLiteral("Hello World!"));
+ QCOMPARE(reply.argumentAt<1>(), true);
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+private Q_SLOTS:
+ void initTestCase() {
+ for (int i = 0; i < 10; ++i) {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ if (iface.isValid()) {
+ return;
+ }
+ QTest::qWait(100);
+ }
+
+ QFAIL("Failed to obtain a valid dbus interface");
+ }
+
+ void cleanupTestCase() {
+ QDBusInterface iface(DBusServer::serviceName, DBusServer::objectPath,
+ DBusServer::interfaceName);
+ iface.call(QStringLiteral("quit"));
+ }
+
+ addTest(Triggers)
+ addCoroAndThenTests(QCoroWrapperTriggers)
+ addCoroAndThenTests(ReturnsResult)
+ addTest(ReturnsBlockingResult)
+ addTest(DoesntBlockEventLoop)
+ addCoroAndThenTests(DoesntCoAwaitFinishedCall)
+ addCoroAndThenTests(HandlesMultipleArguments)
+};
+
+DBUS_TEST_MAIN(QCoroDBusPendingCallTest)
+
+#include "qdbuspendingreply.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcorofuture.h"
+
+#include <QString>
+#include <QException>
+#include <QtConcurrentRun>
+#if QT_VERSION_MAJOR > 6
+#include <QPromise>
+#endif
+
+#include <thread>
+
+class TestException : public QException {
+public:
+ explicit TestException(const QString &msg)
+ : mMsg(msg)
+ {}
+ const char *what() const noexcept override { return qUtf8Printable(mMsg); }
+
+ TestException *clone() const override {
+ return new TestException(mMsg);
+ }
+
+ void raise() const override {
+ throw *this;
+ }
+
+private:
+ QString mMsg;
+};
+
+class MoveOnly
+{
+public:
+ explicit MoveOnly(int value)
+ : value(value)
+ {}
+ MoveOnly(const MoveOnly &) = delete;
+ MoveOnly(MoveOnly &&) = default;
+ MoveOnly &operator=(const MoveOnly &) = delete;
+ MoveOnly &operator=(MoveOnly &&) = default;
+ ~MoveOnly() = default;
+
+ int value = 0;
+};
+
+class QCoroFutureTest : public QCoro::TestObject<QCoroFutureTest> {
+ Q_OBJECT
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ auto future = QtConcurrent::run([] { std::this_thread::sleep_for(100ms); });
+ co_await future;
+
+ QCORO_VERIFY(future.isFinished());
+ }
+
+ QCoro::Task<> testQCoroWrapperTriggers_coro(QCoro::TestContext) {
+ auto future = QtConcurrent::run([] { std::this_thread::sleep_for(100ms); });
+ co_await qCoro(future).waitForFinished();
+
+ QCORO_VERIFY(future.isFinished());
+ }
+
+ void testThenQCoroWrapperTriggers_coro(TestLoop &el) {
+ auto future = QtConcurrent::run([] { std::this_thread::sleep_for(100ms); });
+
+ bool called = false;
+ qCoro(future).waitForFinished().then([&]() {
+ called = true;
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testReturnsResult_coro(QCoro::TestContext) {
+ const QString result = co_await QtConcurrent::run([] {
+ std::this_thread::sleep_for(100ms);
+ return QStringLiteral("42");
+ });
+
+ QCORO_COMPARE(result, QStringLiteral("42"));
+ }
+
+ void testThenReturnsResult_coro(TestLoop &el) {
+ const auto future = QtConcurrent::run([] {
+ std::this_thread::sleep_for(100ms);
+ return QStringLiteral("42");
+ });
+
+ bool called = false;
+ qCoro(future).waitForFinished().then([&](const QString &result) {
+ called = true;
+ el.quit();
+ QCOMPARE(result, QStringLiteral("42"));
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testDoesntBlockEventLoop_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive;
+
+ co_await QtConcurrent::run([] { std::this_thread::sleep_for(500ms); });
+
+ QCORO_VERIFY(eventLoopResponsive);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitFinishedFuture_coro(QCoro::TestContext test) {
+ auto future = QtConcurrent::run([] { std::this_thread::sleep_for(100ms); });
+ co_await future;
+
+ QCORO_VERIFY(future.isFinished());
+
+ test.setShouldNotSuspend();
+ co_await future;
+ }
+
+ void testThenDoesntCoAwaitFinishedFuture_coro(TestLoop &el) {
+ auto future = QtConcurrent::run([] { std::this_thread::sleep_for(1ms); });
+ QTest::qWait((100ms).count());
+ QVERIFY(future.isFinished());
+
+ bool called = false;
+ qCoro(future).waitForFinished().then([&]() {
+ called = true;
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitCanceledFuture_coro(QCoro::TestContext test) {
+ test.setShouldNotSuspend();
+
+ QFuture<void> future;
+ co_await future;
+ }
+
+ void testThenDoesntCoAwaitCanceledFuture_coro(TestLoop &el) {
+ QFuture<void> future;
+ bool called = false;
+ qCoro(future).waitForFinished().then([&]() {
+ called = true;
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+ QCoro::Task<> testPropagateQExceptionFromVoidConcurrent_coro(QCoro::TestContext) {
+ auto future = QtConcurrent::run([]() {
+ std::this_thread::sleep_for(100ms);
+ throw TestException(QStringLiteral("Ooops"));
+ });
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await future, TestException);
+ }
+
+ QCoro::Task<> testPropagateQExceptionFromNonvoidConcurrent_coro(QCoro::TestContext) {
+ bool throwException = true;
+ auto future = QtConcurrent::run([throwException]() -> int {
+ std::this_thread::sleep_for(100ms);
+ if (throwException) { // Workaround MSVC reporting the "return" stmt as unreachablet
+ throw TestException(QStringLiteral("Ooops"));
+ }
+ return 42;
+ });
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await future, TestException);
+ }
+
+#if QT_VERSION_MAJOR >= 6
+ QCoro::Task<> testPropagateQExceptionFromVoidPromise_coro(QCoro::TestContext) {
+ QPromise<void> promise;
+ QTimer::singleShot(100ms, this, [&promise]() {
+ promise.start();
+ promise.setException(TestException(QStringLiteral("Booom")));
+ promise.finish();
+ });
+
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await promise.future(), TestException);
+ }
+
+ QCoro::Task<> testPropagateQExceptionFromNonvoidPromise_coro(QCoro::TestContext) {
+ QPromise<int> promise;
+ QTimer::singleShot(100ms, this, [&promise]() {
+ promise.start();
+ promise.setException(TestException(QStringLiteral("Booom")));
+ promise.finish();
+ });
+
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await promise.future(), TestException);
+ }
+
+ QCoro::Task<> testPropagateStdExceptionFromVoidPromise_coro(QCoro::TestContext) {
+ QPromise<void> promise;
+ QTimer::singleShot(100ms, this, [&promise]() {
+ promise.start();
+ promise.setException(std::make_exception_ptr(std::runtime_error("Booom")));
+ promise.finish();
+ });
+
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await promise.future(), std::runtime_error);
+ }
+
+ QCoro::Task<> testPropagateStdExceptionFromNonvoidPromise_coro(QCoro::TestContext) {
+ QPromise<void> promise;
+ QTimer::singleShot(100ms, this, [&promise]() {
+ promise.start();
+ promise.setException(std::make_exception_ptr(std::runtime_error("Booom")));
+ promise.finish();
+ });
+
+ QCORO_VERIFY_EXCEPTION_THROWN(co_await promise.future(), std::runtime_error);
+ }
+
+ QCoro::Task<> testTakeResult_coro(QCoro::TestContext) {
+ auto future = QtConcurrent::run([]() -> MoveOnly {
+ std::this_thread::sleep_for(10ms);
+ return MoveOnly(42);
+ });
+
+ MoveOnly result = co_await qCoro(future).takeResult();
+ QCORO_COMPARE(result.value, 42);
+
+ QPromise<MoveOnly> promise;
+ QTimer::singleShot(10ms, this, [&promise]() {
+ promise.start();
+ promise.addResult(MoveOnly(84));
+ promise.finish();
+ });
+
+ QCORO_COMPARE((co_await qCoro(promise.future()).takeResult()).value, 84);
+ }
+
+ void testThenTakeResult_coro(TestLoop &el) {
+ auto future = QtConcurrent::run([]() -> MoveOnly {
+ std::this_thread::sleep_for(10ms);
+ return MoveOnly(42);
+ });
+
+ bool called = false;
+ qCoro(future).takeResult().then([&](MoveOnly result) {
+ called = true;
+ QCOMPARE(result.value, 42);
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(called);
+ }
+
+#endif
+
+// QPromise cancelling running future on destruction has been introduced in
+// Qt 6.3.
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 1)
+ QCoro::Task<> testUnfinishedPromiseDestroyed_coro(QCoro::TestContext) {
+ const auto future = [this]() {
+ auto promise = std::make_shared<QPromise<int>>();
+ auto future = promise->future();
+
+ QTimer::singleShot(400ms, this, [p = promise]() {
+ p->addResult(42);
+ });
+ return future;
+ }();
+ co_await future;
+ }
+#endif
+
+private Q_SLOTS:
+ addTest(Triggers)
+ addCoroAndThenTests(ReturnsResult)
+ addTest(DoesntBlockEventLoop)
+ addCoroAndThenTests(DoesntCoAwaitFinishedFuture)
+ addCoroAndThenTests(DoesntCoAwaitCanceledFuture)
+ addCoroAndThenTests(QCoroWrapperTriggers)
+ addTest(PropagateQExceptionFromVoidConcurrent)
+ addTest(PropagateQExceptionFromNonvoidConcurrent)
+#if QT_VERSION_MAJOR >= 6
+ addTest(PropagateQExceptionFromVoidPromise)
+ addTest(PropagateQExceptionFromNonvoidPromise)
+ addTest(PropagateStdExceptionFromVoidPromise)
+ addTest(PropagateStdExceptionFromNonvoidPromise)
+ addCoroAndThenTests(TakeResult)
+#endif
+#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 1)
+ addTest(UnfinishedPromiseDestroyed)
+#endif
+};
+
+QTEST_GUILESS_MAIN(QCoroFutureTest)
+
+#include "qfuture.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+#include "qcorotimer.h"
+
+#include <chrono>
+
+#include <QElapsedTimer>
+
+using namespace std::chrono_literals;
+
+class QCoroTimerTest : public QCoro::TestObject<QCoroTimerTest> {
+ Q_OBJECT
+
+private:
+ QCoro::Task<> testTriggers_coro(QCoro::TestContext) {
+ QTimer timer;
+ timer.setInterval(100ms);
+ timer.start();
+
+ co_await timer;
+ }
+
+ QCoro::Task<> testQCoroWrapperTriggers_coro(QCoro::TestContext) {
+ QTimer timer;
+ timer.setInterval(100ms);
+ timer.start();
+
+ co_await qCoro(timer).waitForTimeout();
+ }
+
+ QCoro::Task<> testDoesntBlockEventLoop_coro(QCoro::TestContext) {
+ QCoro::EventLoopChecker eventLoopResponsive;
+
+ QTimer timer;
+ timer.setInterval(500ms);
+ timer.start();
+
+ co_await timer;
+
+ QCORO_VERIFY(eventLoopResponsive);
+ }
+
+ QCoro::Task<> testDoesntCoAwaitInactiveTimer_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ QTimer timer;
+ timer.setInterval(1s);
+ // Don't start the timer!
+
+ co_await timer;
+ }
+
+ QCoro::Task<> testDoesntCoAwaitNullTimer_coro(QCoro::TestContext ctx) {
+ ctx.setShouldNotSuspend();
+
+ QTimer *timer = nullptr;
+
+ co_await timer;
+ }
+
+ void testThenTriggers_coro(TestLoop &el) {
+ QTimer timer;
+ bool triggered = false;
+ timer.start(10ms);
+ qCoro(timer).waitForTimeout().then([&el, &triggered]() {
+ triggered = true;
+ el.quit();
+ });
+ el.exec();
+ QVERIFY(triggered);
+ }
+
+ QCoro::Task<> testSleepFor_coro(QCoro::TestContext) {
+ QElapsedTimer elapsed;
+ elapsed.start();
+ co_await QCoro::sleepFor(100ms);
+ QCORO_VERIFY(elapsed.elapsed() >= 75);
+ }
+
+ QCoro::Task<> testSleepUntil_coro(QCoro::TestContext) {
+ QElapsedTimer elapsed;
+ elapsed.start();
+ co_await QCoro::sleepUntil(std::chrono::steady_clock::now() + 500ms);
+ QCORO_VERIFY(elapsed.elapsed() >= 475);
+ }
+
+private Q_SLOTS:
+ addTest(Triggers)
+ addTest(QCoroWrapperTriggers)
+ addTest(DoesntBlockEventLoop)
+ addTest(DoesntCoAwaitInactiveTimer)
+ addTest(DoesntCoAwaitNullTimer)
+ addTest(SleepFor)
+ addTest(SleepUntil)
+
+ addThenTest(Triggers)
+};
+
+QTEST_GUILESS_MAIN(QCoroTimerTest)
+
+#include "qtimer.moc"
--- /dev/null
+#include <QObject>
+#include <QTest>
+
+#include "qcoro/coroutine.h"
+#include "qcoro/qcorotask.h"
+#include "qcoro/qcorolazytask.h"
+
+namespace helper {
+
+template<QCoro::Awaitable T>
+struct is_awaitable {
+ static constexpr bool value = true;
+};
+
+template<QCoro::Awaitable T>
+static constexpr bool is_awaitable_v = is_awaitable<T>::value;
+
+} // namespace helper
+
+struct TestAwaitable {
+ bool await_ready() const { return true; }
+ void await_suspend(std::coroutine_handle<>);
+ void await_resume() const;
+};
+
+struct TestAwaitableWithOperatorCoAwait {
+ TestAwaitable operator co_await() {
+ return TestAwaitable{};
+ }
+};
+
+namespace TestNS {
+
+struct TestAwaitableWithNonmemberOperatorCoAwait {};
+
+#if !defined(_MSC_VER) || defined(__clang__)
+// This is how it's supposed to work: the operator must be defined in the same namespace
+// as the argument type and is discovered via ADL.
+auto operator co_await(TestAwaitableWithNonmemberOperatorCoAwait &&) {
+ return TestAwaitable{};
+}
+#endif
+
+} // namespace TestNS
+
+#if defined(_MSC_VER) && !defined(__clang__)
+// Unfortunately, MSVC is only able to find the operator in global namespace :(
+// This is most likely a bug in MSVC.
+auto operator co_await(TestNS::TestAwaitableWithNonmemberOperatorCoAwait &&) {
+ return TestAwaitable{};
+}
+#endif
+
+class TestConstraints : public QObject {
+ Q_OBJECT
+private Q_SLOTS:
+ void testAwaitableConcept() {
+
+ static_assert(helper::is_awaitable_v<QCoro::Task<void>>,
+ "Awaitable concept doesn't accept Task<void>, although it should.");
+ static_assert(helper::is_awaitable_v<QCoro::Task<int>>,
+ "Awaitable concept doesn't accept Task<T>, although it should.");
+ static_assert(helper::is_awaitable_v<QCoro::LazyTask<void>>,
+ "Awaitable concept doesn't accept LazyTask<void>, although it should.");
+ static_assert(helper::is_awaitable_v<QCoro::LazyTask<void>>,
+ "Awaitable concept doesn't accept LazyTask<T>, although it should.");
+ static_assert(helper::is_awaitable_v<TestAwaitable>,
+ "Awaitable concept doesn't accept an awaitable with await member functions.");
+ static_assert(helper::is_awaitable_v<TestAwaitableWithOperatorCoAwait>,
+ "Awaitable concept doesn't accept an awaitable with member operator co_await.");
+ static_assert(helper::is_awaitable_v<TestNS::TestAwaitableWithNonmemberOperatorCoAwait>,
+ "Awaitable concept doesn't accept an awaitable with non-member operator co_await.");
+ }
+};
+
+QTEST_GUILESS_MAIN(TestConstraints)
+
+#include "testconstraints.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testlibs/testhttpserver.h"
+
+#include <QDebug>
+#include <QEventLoop>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QTcpServer>
+#include <QTest>
+#include <QTimer>
+
+Q_DECLARE_METATYPE(std::chrono::seconds)
+
+using namespace std::chrono_literals;
+
+class TestHttpServerTest : public QObject {
+ Q_OBJECT
+
+private Q_SLOTS:
+ void init() {
+ mServer.start(QHostAddress::LocalHost);
+ }
+
+ void cleanup() {
+ mServer.stop();
+ }
+
+ void testGet_data() {
+ QTest::addColumn<QString>("url");
+ QTest::addColumn<QByteArray>("expectedData");
+ QTest::addColumn<std::chrono::seconds>("timeout");
+
+ QTest::newRow("/") << QStringLiteral("http://localhost:%1/") << QByteArray{"abcdef"} << 5s;
+
+ QTest::newRow("/block") << QStringLiteral("http://localhost:%1/block")
+ << QByteArray{"abcdef"} << 5s;
+
+ QTest::newRow("/stream") << QStringLiteral("http://localhost:%1/stream")
+ << QByteArray{"Hola 0\nHola 1\nHola 2\nHola 3\nHola 4\nHola "
+ "5\nHola 6\nHola 7\nHola 8\nHola 9\n"}
+ << 15s;
+ }
+
+ void testGet() {
+ QFETCH(QString, url);
+ QFETCH(QByteArray, expectedData);
+ QFETCH(std::chrono::seconds, timeout);
+
+ QNetworkAccessManager nam;
+ QEventLoop el;
+
+ auto reply = std::unique_ptr<QNetworkReply>(
+ nam.get(QNetworkRequest{QUrl{url.arg(mServer.port())}}));
+ connect(reply.get(), &QNetworkReply::finished, &el, &QEventLoop::quit);
+
+ QTimer::singleShot(timeout, &el, [&el]() mutable { el.exit(1); });
+
+ QCOMPARE(el.exec(), 0);
+
+ QCOMPARE(reply->readAll(), expectedData);
+ }
+
+private:
+ TestHttpServer<QTcpServer> mServer;
+};
+
+QTEST_GUILESS_MAIN(TestHttpServerTest)
+
+#include "testhttpserver.moc"
--- /dev/null
+add_library(qcoro_testlib
+ STATIC
+ testobject.cpp
+ testloop.cpp
+)
+target_include_directories(qcoro_testlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_link_libraries(qcoro_testlib PUBLIC
+ QCoro${QT_VERSION_MAJOR}Core
+ QCoro${QT_VERSION_MAJOR}Test
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Test
+)
+
+if (QCORO_WITH_QTDBUS)
+ add_executable(testdbusserver EXCLUDE_FROM_ALL testdbusserver.cpp)
+ target_link_libraries(testdbusserver
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::DBus
+ )
+
+ set(qcoro_test_dbus_SRCS)
+ if (${QT_VERSION_MAJOR} EQUAL 5)
+ qt5_add_dbus_interface(qcoro_test_dbus_SRCS cz.dvratil.qcorodbustest.xml qcorodbustestinterface)
+ else()
+ qt_add_dbus_interface(qcoro_test_dbus_SRCS cz.dvratil.qcorodbustest.xml qcorodbustestinterface)
+ endif()
+
+ add_library(qcoro_test_dbus OBJECT ${qcoro_test_dbus_SRCS})
+ target_include_directories(qcoro_test_dbus PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ )
+ target_link_libraries(qcoro_test_dbus PUBLIC
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::DBus
+ )
+endif()
+
+if (QCORO_WITH_QTWEBSOCKETS)
+ set(qcoro_test_ws_SRCS testwsserver.cpp)
+ add_library(qcoro_test_ws OBJECT ${qcoro_test_ws_SRCS})
+ target_include_directories(qcoro_test_ws PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+ target_link_libraries(qcoro_test_ws PUBLIC
+ Qt${QT_VERSION_MAJOR}::Core
+ Qt${QT_VERSION_MAJOR}::Test
+ Qt${QT_VERSION_MAJOR}::WebSockets
+ )
+endif()
--- /dev/null
+<!DOCTYPE node PUBLIC "-//freedesktop/DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="cz.dvratil.qcorodbustest">
+ <method name="foo">
+ </method>
+
+ <method name="ping">
+ <arg name="ping" type="s" direction="in" />
+ <arg type="s" direction="out" />
+ </method>
+
+ <method name="blockFor">
+ <arg name="seconds" type="i" direction="in" />
+ </method>
+
+ <method name="blockAndReturn">
+ <arg name="seconds" type="i" direction="in" />
+ <arg type="s" direction="out" />
+ </method>
+
+ <method name="blockAndReturnMultipleArguments">
+ <arg name="seconds" type="i" direction="in" />
+ <arg type="s" direction="out" />
+ <arg type="b" direction="out" />
+ </method>
+
+ <method name="quit">
+ <annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
+ </method>
+ </interface>
+</node>
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testdbusserver.h"
+
+#include <QCoreApplication>
+#include <QDBusConnection>
+#include <QDBusError>
+#include <QDebug>
+#include <QTimer>
+
+#include <thread>
+
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+DBusServer::DBusServer() {
+ QTimer::singleShot(0, this, &DBusServer::run);
+ // Self-terminate if there's no interaction from a client in 30 seconds.
+ // Prevents leaking the test server in case the test crashes.
+ mSuicideTimer.setInterval(30s);
+ mSuicideTimer.setSingleShot(true);
+ connect(&mSuicideTimer, &QTimer::timeout, this, []() {
+ std::cerr << "No call in 30 seconds, terminating!" << std::endl;
+ qApp->exit(1);
+ });
+}
+
+void DBusServer::run() {
+ auto conn = QDBusConnection::sessionBus();
+ if (!conn.registerService(serviceName)) {
+ qWarning() << "Failed to register service to DBus:" << conn.lastError().message();
+ }
+ if (!conn.registerObject(objectPath, interfaceName, this, QDBusConnection::ExportAllSlots)) {
+ qWarning() << "Failed to register object to DBus" << conn.lastError().message();
+ }
+
+ mSuicideTimer.start();
+}
+
+void DBusServer::foo() {
+ mSuicideTimer.start();
+}
+
+void DBusServer::blockFor(int seconds) {
+ std::this_thread::sleep_for(std::chrono::seconds(seconds));
+ mSuicideTimer.start();
+}
+
+QString DBusServer::blockAndReturn(int seconds) {
+ std::this_thread::sleep_for(std::chrono::seconds(seconds));
+ mSuicideTimer.start();
+ return QStringLiteral("Slept for %1 seconds").arg(seconds);
+}
+
+QString DBusServer::blockAndReturnMultipleArguments(int seconds, bool &out) {
+ std::this_thread::sleep_for(std::chrono::seconds{seconds});
+ mSuicideTimer.start();
+ out = true;
+ return QStringLiteral("Hello World!");
+}
+
+QString DBusServer::ping(const QString &ping) {
+ mSuicideTimer.start();
+ return ping;
+}
+
+void DBusServer::quit() {
+ mSuicideTimer.stop();
+ qApp->quit();
+}
+
+int main(int argc, char **argv) {
+ QCoreApplication app(argc, argv);
+ DBusServer server;
+ return app.exec();
+}
+
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include <QCoreApplication>
+#include <QObject>
+#include <QProcess>
+#include <QTimer>
+
+#include <QString>
+
+#include <iostream>
+
+class DBusServer : public QObject {
+ Q_OBJECT
+
+public:
+ inline static const QString serviceName = QStringLiteral("cz.dvratil.qcorodbustest");
+ inline static const QString interfaceName = QStringLiteral("cz.dvratil.qcorodbustest");
+ inline static const QString objectPath = QStringLiteral("/");
+
+ explicit DBusServer();
+
+ void run();
+
+public Q_SLOTS:
+
+ void foo();
+ QString ping(const QString &ping);
+
+ void blockFor(int seconds);
+ QString blockAndReturn(int seconds);
+ QString blockAndReturnMultipleArguments(int seconds, bool &out);
+
+ void quit();
+
+private:
+ QTimer mSuicideTimer;
+};
+
+// We must run the DBus server into its own process due to QTBUG-92107 (asyncCall blocks if the
+// remote service is registered in the same process (even if it lives in a different thread)
+#define DBUS_TEST_MAIN(TestClass) \
+ bool startDBusServer(QProcess &process) { \
+ process.start(QStringLiteral(TESTDBUSSERVER_EXECUTABLE), QStringList{}); \
+ if (!process.waitForStarted()) { \
+ std::cerr << "Failed to start testdbusserver" << std::endl; \
+ return false; \
+ } \
+ return true; \
+ } \
+ \
+ bool stopDBusServer(QProcess &process) { \
+ process.waitForFinished(); \
+ if (process.exitCode() != 0) { \
+ std::cerr << "testdbuserver terminated with exit code " << process.exitCode() \
+ << std::endl; \
+ return false; \
+ } \
+ return true; \
+ } \
+ \
+ int main(int argc, char **argv) { \
+ QCoreApplication app(argc, argv); \
+ QProcess dbusServer; \
+ if (!startDBusServer(dbusServer)) \
+ return 1; \
+ TestClass testClass; \
+ QTEST_SET_MAIN_SOURCE_PATH \
+ const int result = QTest::qExec(&testClass, argc, argv); \
+ if (!stopDBusServer(dbusServer)) \
+ return 1; \
+ return result; \
+ }
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QDebug>
+#include <QThread>
+#include <QTest>
+
+#include <condition_variable>
+#include <mutex>
+#include <atomic>
+
+class QTcpServer;
+class QTcpSocket;
+class QLocalServer;
+class QLocalSocket;
+
+template<typename Func>
+class Thread : public QThread {
+public:
+ explicit Thread(Func &&f) : mFunc(std::forward<Func>(f)) {}
+
+ ~Thread() = default;
+
+ void run() override {
+ mFunc();
+ }
+
+private:
+ Func mFunc;
+};
+
+template<typename Func>
+Thread(Func &&) -> Thread<Func>;
+
+template<typename ServerType>
+struct socket_for_server {};
+
+template<> struct socket_for_server<QTcpServer> {
+ using type = QTcpSocket;
+};
+template<> struct socket_for_server<QLocalServer> {
+ using type = QLocalSocket;
+};
+
+template<typename ServerType>
+class TestHttpServer {
+ using SocketType = typename socket_for_server<ServerType>::type;
+
+public:
+ template<typename T>
+ void start(const T &name) {
+ mPort = 0;
+ mHasConnection = false;
+ mStop = false;
+ mExpectTimeout = false;
+ // Can't use QThread::create, it's only available when Qt is built with C++17,
+ // which some distros don't have :(
+ mThread.reset(new Thread([this, name]() { run(name); }));
+ mThread->start();
+ std::unique_lock lock(mReadyMutex);
+ mServerReady.wait(lock, [this]() { return mPort != 0; });
+ }
+
+ void stop() {
+ mStop = true;
+ if (mThread->isRunning()) {
+ mThread->wait();
+ }
+ mThread.reset();
+ mPort = 0;
+ mHasConnection = false;
+ }
+
+ uint16_t port() const {
+ return mPort;
+ }
+
+ void setExpectTimeout(bool expectTimeout) {
+ mExpectTimeout = expectTimeout;
+ }
+
+ bool waitForConnection() {
+ std::unique_lock lock(mReadyMutex);
+ return mServerReady.wait_for(lock, std::chrono::seconds(5), [this]() { return mHasConnection; });
+ }
+
+private:
+ template<typename T>
+ void run(const T &name) {
+ using namespace std::chrono_literals;
+
+ ServerType server{};
+ if (!server.listen(name)) {
+ qDebug() << "Error listening:" << server.serverError();
+ return;
+ }
+ assert(server.isListening());
+
+ {
+ std::scoped_lock lock(mReadyMutex);
+ if constexpr (std::is_same_v<ServerType, QTcpServer>) {
+ mPort = server.serverPort();
+ } else {
+ mPort = 1;
+ }
+ }
+
+ SocketType *conn = nullptr;
+ mServerReady.notify_all();
+ for (int i = 0; i < 10 && !mStop; ++i) {
+ if (server.waitForNewConnection(1000)) {
+ conn = server.nextPendingConnection();
+ break;
+ }
+ }
+
+ if (!conn) {
+ if (!mExpectTimeout) {
+ QFAIL("No incoming connection within timeout!");
+ }
+ mPort = 0;
+ return;
+ }
+
+ mHasConnection = true;
+ mServerReady.notify_all();
+
+ if (conn->waitForReadyRead(10000)) {
+ const auto request = conn->readLine();
+ qDebug() << request;
+ if (request == "GET /stream HTTP/1.1\r\n") {
+ QStringList lines;
+ for (int i = 0; i < 10; ++i) {
+ lines.push_back(QStringLiteral("Hola %1\n").arg(i));
+ }
+
+ const auto len =
+ std::accumulate(lines.cbegin(), lines.cend(), 0,
+ [](int l, const QString &s) { return l + s.size(); });
+ conn->write("HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/plain\r\n"
+ "Content-Length: " +
+ QByteArray::number(len) +
+ "\r\n"
+ "\r\n");
+ conn->flush();
+ for (const auto &line : lines) {
+ conn->write(line.toUtf8());
+ conn->flush();
+ std::this_thread::sleep_for(100ms);
+ }
+ } else {
+ if (request == "GET /block HTTP/1.1\r\n") {
+ std::this_thread::sleep_for(500ms);
+ }
+
+ conn->write("HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n"
+ "abcdef");
+ }
+ conn->flush();
+ conn->close();
+ } else if (!mStop) {
+ if (conn->state() == std::remove_cvref_t<decltype(*conn)>::ConnectedState) {
+ if (!mExpectTimeout) {
+ QFAIL("No request within 10 seconds");
+ }
+ } else {
+ qDebug() << "Client disconnected without sending request";
+ }
+ }
+
+ delete conn;
+ mPort = 0;
+ }
+
+ std::unique_ptr<QThread> mThread;
+ std::mutex mReadyMutex;
+ std::condition_variable mServerReady;
+ uint16_t mPort = 0;
+ bool mHasConnection = false;
+ std::atomic_bool mStop = false;
+ std::atomic_bool mExpectTimeout = false;
+};
--- /dev/null
+#include "testloop.h"
+
+#include <QTest>
+
+TestLoop::TestLoop(QObject *parent)
+ : QObject(parent)
+{
+ mTimer.setSingleShot(true);
+ connect(&mTimer, &QTimer::timeout, this, [this]() {
+ mEventLoop.quit();
+ QFAIL("Test timeout!");
+ });
+}
+
+void TestLoop::exec() {
+ mEventLoop.exec();
+}
+
+void TestLoop::quit() {
+ mTimer.stop();
+ QTimer::singleShot(0, &mEventLoop, &QEventLoop::quit);
+}
--- /dev/null
+#pragma once
+
+#include <QEventLoop>
+#include <QTimer>
+
+#define DELAYED(expr) \
+ QTimer::singleShot(10ms, [&]() { expr; })
+
+class TestLoop : public QObject {
+ Q_OBJECT
+public:
+ explicit TestLoop(QObject *parent = nullptr);
+
+ void exec();
+ void quit();
+
+private:
+ QEventLoop mEventLoop;
+ QTimer mTimer;
+};
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+//! Executes given \c expr with 10ms delay.
+#define QCORO_DELAY(expr) \
+ QTimer::singleShot(10ms, [&]() { expr; })
+
+#define QCORO_TEST_TIMEOUT(expr) { \
+ const auto start = std::chrono::steady_clock::now(); \
+ const bool ok = expr; \
+ const auto end = std::chrono::steady_clock::now(); \
+ QCORO_VERIFY(!ok); \
+ QCORO_VERIFY((end - start) < 500ms); \
+}
+
+
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testobject.h"
+
+using namespace QCoro;
+
+TestContext::TestContext(QEventLoop &el) : mEventLoop(&el) {
+ mEventLoop->setProperty("testFinished", false);
+ mEventLoop->setProperty("shouldNotSuspend", false);
+}
+
+TestContext::TestContext(TestContext &&other) noexcept {
+ std::swap(mEventLoop, other.mEventLoop);
+}
+
+TestContext::~TestContext() {
+ if (mEventLoop) {
+ mEventLoop->setProperty("testFinished", true);
+ mEventLoop->quit();
+ }
+}
+
+TestContext &TestContext::operator=(TestContext &&other) noexcept {
+ std::swap(mEventLoop, other.mEventLoop);
+ return *this;
+}
+
+void TestContext::setShouldNotSuspend() {
+ mEventLoop->setProperty("shouldNotSuspend", true);
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QEventLoop>
+#include <QObject>
+#include <QTest>
+#include <QTimer>
+#include <QVariant>
+
+#include "qcorotask.h"
+#include "qcorotest.h"
+#include "testmacros.h"
+#include "testloop.h"
+
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+namespace QCoro {
+
+class TestContext {
+public:
+ TestContext(QEventLoop &el);
+ TestContext(TestContext &&) noexcept;
+ TestContext(const TestContext &) = delete;
+
+ ~TestContext();
+
+ TestContext &operator=(TestContext &&) noexcept;
+ TestContext &operator=(const TestContext &) = delete;
+
+ void setShouldNotSuspend();
+
+private:
+ QEventLoop *mEventLoop = {};
+};
+
+class EventLoopChecker : public QTimer {
+ Q_OBJECT
+public:
+ explicit EventLoopChecker(int minTicks = 10, std::chrono::milliseconds interval = 5ms)
+ : mMinTicks{minTicks} {
+ connect(this, &EventLoopChecker::timeout, this, [this]() { ++mTick; });
+ setInterval(interval);
+ start();
+ }
+
+ operator bool() const {
+ if (mTick < mMinTicks) {
+ qDebug() << "EventLoopChecker failed: ticks=" << mTick << ", minTicks=" << mMinTicks;
+ }
+ return mTick >= mMinTicks;
+ }
+
+private:
+ int mTick = 0;
+ int mMinTicks = 10;
+};
+
+template<typename TestClass>
+class TestObject : public QObject {
+protected:
+ explicit TestObject(QObject *parent = nullptr)
+ : QObject(parent)
+ {}
+
+ void coroWrapper(QCoro::Task<> (TestClass::*testFunction)(TestContext)) {
+ QEventLoop el;
+ QTimer::singleShot(5s, &el, [&el]() mutable { el.exit(1); });
+
+ (static_cast<TestClass *>(this)->*testFunction)(el);
+
+ bool testFinished = el.property("testFinished").toBool();
+ const bool shouldNotSuspend = el.property("shouldNotSuspend").toBool();
+ if (testFinished) {
+ QVERIFY(shouldNotSuspend);
+ } else {
+ QVERIFY(!shouldNotSuspend);
+
+ const auto result = el.exec();
+ QVERIFY2(result == 0, "Test function has timed out");
+
+ testFinished = el.property("testFinished").toBool();
+ QVERIFY(testFinished);
+ }
+ }
+};
+
+#define addTest(name) \
+ void test##name() { \
+ coroWrapper(&std::remove_cvref_t<decltype(*this)>::test##name##_coro); \
+ }
+
+
+#define addThenTest(name) \
+ void testThen##name() { \
+ TestLoop loop; \
+ testThen##name##_coro(loop); \
+ }
+
+#define addCoroAndThenTests(name) \
+ addTest(name) \
+ addThenTest(name)
+
+} // namespace QCoro
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#include "testwsserver.h"
+
+#include <QWebSocket>
+#include <QWebSocketServer>
+#include <QDebug>
+#include <QTimer>
+#include <QRandomGenerator>
+#include <QTest>
+
+#include <chrono>
+#include <cstring>
+
+using namespace std::chrono_literals;
+
+class Server : public QObject {
+ Q_OBJECT
+public:
+ explicit Server() = default;
+
+ void setExpectTimeout() {
+ mExpectTimeout = true;
+ }
+
+ QUrl waitForStart() {
+ std::unique_lock lock(mMutex);
+ mCond.wait(lock, [this]() { return !mUrl.isEmpty(); });
+ return mUrl;
+ }
+
+ bool waitForConnection() {
+ std::unique_lock lock(mMutex);
+ if (!mSocket) {
+ return mCond.wait_for(lock, 5s, [this]() -> bool { return mSocket.get() != nullptr; });
+ }
+ return true;
+
+ }
+
+ void start() {
+ mServer.reset(new QWebSocketServer(QStringLiteral("QCoroTestWSServer"), QWebSocketServer::NonSecureMode));
+ if (!mServer->listen(QHostAddress::LocalHost)) {
+ qCritical() << "WebSocket server failed to start listening";
+ close();
+ return;
+ }
+
+ std::scoped_lock lock(mMutex);
+ mUrl = mServer->serverUrl();
+
+ mTimeout.reset(new QTimer());
+ connect(mTimeout.get(), &QTimer::timeout, this, &Server::newConnectionTimeout);
+ mTimeout->setSingleShot(true);
+ mTimeout->start(10s);
+
+ connect(mServer.get(), &QWebSocketServer::newConnection,
+ this, &Server::newConnection);
+ connect(mServer.get(), &QWebSocketServer::acceptError,
+ this, [this](auto error) {
+ qCritical() << "WebSocket server failed to accept incoming connection:" << error;
+ close();
+ });
+ connect(mServer.get(), &QWebSocketServer::serverError,
+ this, [this](auto error) {
+ qCritical() << "WebSocket server failed to set up WS connection" << error;
+ close();
+ });
+
+ mCond.notify_all();
+ }
+
+ void close() {
+ QThread::currentThread()->quit();
+ mSocket.reset();
+ mTimeout.reset();
+ mServer.reset();
+ }
+private Q_SLOTS:
+ void newConnectionTimeout() {
+ if (!mExpectTimeout) {
+ QFAIL("No incoming connection within timeout");
+ }
+ close();
+ }
+
+ void newConnection() {
+ mTimeout->stop();
+
+ {
+ std::scoped_lock lock(mMutex);
+ mSocket.reset(mServer->nextPendingConnection());
+ }
+ mCond.notify_all();
+
+ mTimeout.reset(new QTimer());
+ connect(mTimeout.get(), &QTimer::timeout, this, [this]() {
+ if (!mExpectTimeout) {
+ QFAIL("No incoming request within timeout");
+ }
+ close();
+ });
+ mTimeout->setSingleShot(true);
+ mTimeout->start(5s);
+
+ connect(mSocket.get(), &QWebSocket::textMessageReceived,
+ this, [this](const QString &msg) {
+ mTimeout->stop();
+ const auto request = mSocket->requestUrl().path();
+ if (request == QLatin1String("/delay")) {
+ std::this_thread::sleep_for(300ms);
+ mSocket->sendTextMessage(msg);
+ } else if (request == QLatin1String("/large")) {
+ const auto response = QString::fromLatin1(generateLargeMessage().toHex());
+ mSocket->sendTextMessage(response);
+ } else {
+ mSocket->sendTextMessage(msg);
+ }
+ });
+ connect(mSocket.get(), &QWebSocket::binaryMessageReceived,
+ this, [this](const QByteArray &msg) {
+ mTimeout->stop();
+ const auto request = mSocket->requestUrl().path();
+ if (request == QLatin1String("/delay")) {
+ std::this_thread::sleep_for(100ms);
+ mSocket->sendBinaryMessage(msg);
+ } else if (request == QLatin1String("/large")) {
+ mSocket->sendBinaryMessage(generateLargeMessage());
+ } else {
+ mSocket->sendBinaryMessage(msg);
+ }
+ });
+ }
+
+private:
+ QByteArray generateLargeMessage() const {
+ constexpr qsizetype size = 10 * 1024 * 1024; /* 10MiB */
+ std::vector<uint64_t> buffer;
+ buffer.resize(size);
+ QRandomGenerator::global()->fillRange(buffer.data(), buffer.size());
+
+ QByteArray msg;
+ msg.resize(size);
+ std::memcpy(msg.data(), buffer.data(), buffer.size());
+ return msg;
+ }
+
+ std::unique_ptr<QWebSocket> mSocket;
+ std::unique_ptr<QWebSocketServer> mServer;
+ std::unique_ptr<QTimer> mTimeout;
+ std::condition_variable mCond;
+ std::mutex mMutex;
+ std::atomic_bool mExpectTimeout;
+ QUrl mUrl;
+};
+
+TestWsServer::TestWsServer() = default;
+TestWsServer::~TestWsServer() = default;
+
+void TestWsServer::start() {
+ mThread.reset(new QThread());
+ mThread->start();
+
+ mServer.reset(new Server());
+ mServer->moveToThread(mThread.get());
+
+ QMetaObject::invokeMethod(mServer.get(), &Server::start, Qt::QueuedConnection);
+
+ mUrl = mServer->waitForStart();
+}
+
+void TestWsServer::stop() {
+ if (mThread && mThread->isRunning()) {
+ QMetaObject::invokeMethod(mServer.get(), &Server::close, Qt::BlockingQueuedConnection);
+ mThread->wait();
+ }
+ mThread.reset();
+ mServer.reset();
+ mUrl.clear();
+}
+
+QUrl TestWsServer::url() const {
+ return mUrl;
+}
+
+void TestWsServer::setExpectTimeout() {
+ mServer->setExpectTimeout();
+}
+
+bool TestWsServer::waitForConnection() {
+ return mServer->waitForConnection();
+}
+
+#include "testwsserver.moc"
--- /dev/null
+// SPDX-FileCopyrightText: 2022 Daniel Vrátil <dvratil@kde.org>
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <QThread>
+#include <QUrl>
+
+#include <memory>
+#include <mutex>
+#include <condition_variable>
+#include <atomic>
+
+class Server;
+class TestWsServer {
+public:
+ TestWsServer();
+ ~TestWsServer();
+
+ void start();
+ void stop();
+ QUrl url() const;
+ bool waitForConnection();
+
+ void setExpectTimeout();
+private:
+ std::unique_ptr<QThread> mThread;
+ std::unique_ptr<Server> mServer;
+ QUrl mUrl;
+};